extraer-datos-ine 1.0.0 → 1.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/README.md +76 -0
- package/dist/index.cjs +128 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +145 -2
- package/dist/index.d.ts +145 -2
- package/dist/index.js +112 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ SDK oficial en JavaScript / TypeScript para la **[API de OCR para INE de Extraer
|
|
|
6
6
|
- 🧩 **Cero dependencias.** Usa el `fetch`/`FormData` nativos de Node 18+ (y navegadores).
|
|
7
7
|
- 🔠 **TypeScript de primera clase**: tipos para los 20 campos y para cada código de error.
|
|
8
8
|
- 📦 Doble build **ESM + CommonJS**.
|
|
9
|
+
- 📲 **Enlaces de captura** y entrega a tus **webhooks** con verificación de firma incluida.
|
|
9
10
|
|
|
10
11
|
📚 Documentación de la API: **https://extraerdatosdeine.com/docs**
|
|
11
12
|
🔑 Consigue una API key con **20 extracciones gratis**: **https://extraerdatosdeine.com/register**
|
|
@@ -68,6 +69,76 @@ await client.extract({ front: { url }, back: { url: backUrl } })
|
|
|
68
69
|
|
|
69
70
|
> `front` y `back` deben ser del **mismo tipo**: la API no mezcla métodos en una sola petición.
|
|
70
71
|
|
|
72
|
+
## Entregar a un destino (webhook, Telegram…)
|
|
73
|
+
|
|
74
|
+
Si configuraste un destino en tu [panel](https://extraerdatosdeine.com/dashboard/destinations), pasa su ID y los datos se le entregan además de volver en la respuesta:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const { data, delivery } = await client.extract({ front, back, destinationId: 'dst_...' })
|
|
78
|
+
|
|
79
|
+
if (delivery && !delivery.succeeded) {
|
|
80
|
+
console.warn('La extracción funcionó pero la entrega falló:', delivery.errorCode, delivery.httpStatus)
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Un destino inexistente o inactivo falla con `DESTINATION_NOT_FOUND` **antes** de gastar un token. Una entrega fallida no hace fallar la extracción: revisa `delivery.succeeded`.
|
|
85
|
+
|
|
86
|
+
## Enlaces de captura (BETA)
|
|
87
|
+
|
|
88
|
+
Genera una URL de un solo uso para que tu cliente fotografíe su INE (o pasaporte) desde su celular. Los datos no vuelven en esta respuesta: llegan a tu destino.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const link = await client.createCaptureLink({
|
|
92
|
+
destinationId: 'dst_...', // requerido
|
|
93
|
+
documentType: 'ine', // 'ine' (default) | 'passport'
|
|
94
|
+
requireBack: true, // pide también el reverso
|
|
95
|
+
reference: 'HAB-204', // tu folio; viaja en el webhook (máx. 80)
|
|
96
|
+
requesterName: 'Hotel Sol', // lo que ve el invitado (máx. 60)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
enviarPorWhatsApp(cliente.telefono, link.url)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- **Un solo uso.** El invitado tiene 15 min para abrirlo y 15 min más, desde que lo abre, para enviar la foto (hasta 3 fotos ilegibles).
|
|
103
|
+
- **`link.url` contiene un token y solo se devuelve una vez**: trátala como una credencial y no la registres en logs.
|
|
104
|
+
- Máximo **20 enlaces vivos** por cuenta (`TOO_MANY_LIVE_LINKS`).
|
|
105
|
+
- Crear el enlace **no gasta tokens**. El escaneo del invitado gasta uno y **se reembolsa** si la extracción o la entrega fallan.
|
|
106
|
+
- Errores: `NOT_FOUND` (enlaces desactivados en tu cuenta), `DESTINATION_NOT_FOUND`, `INVALID_NAME`, `INVALID_REFERENCE`, `INVALID_REQUESTER_NAME`, `INVALID_DOCUMENT_TYPE`, `TOO_MANY_LIVE_LINKS`.
|
|
107
|
+
|
|
108
|
+
## Recibir webhooks
|
|
109
|
+
|
|
110
|
+
Cada entrega es un `POST` JSON firmado con el secreto del destino (`whsec_…`, mostrado una vez al crearlo). Verifica la firma con el **cuerpo crudo**:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import express from 'express'
|
|
114
|
+
import { parseWebhook, IneExtractorError } from 'extraer-datos-ine'
|
|
115
|
+
|
|
116
|
+
const app = express()
|
|
117
|
+
|
|
118
|
+
app.post('/webhooks/ine', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
const event = await parseWebhook({
|
|
121
|
+
secret: process.env.INE_WEBHOOK_SECRET!,
|
|
122
|
+
rawBody: req.body, // Buffer: NO uses express.json() en esta ruta
|
|
123
|
+
signature: req.get('X-Signature'),
|
|
124
|
+
timestamp: req.get('X-Signature-Timestamp'),
|
|
125
|
+
})
|
|
126
|
+
// event.idempotencyKey se repite en reintentos: ignora duplicados
|
|
127
|
+
if (event.event === 'extraction.completed') {
|
|
128
|
+
console.log(event.source, event.reference, event.data.curp)
|
|
129
|
+
}
|
|
130
|
+
res.sendStatus(200)
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (err instanceof IneExtractorError && err.code === 'INVALID_SIGNATURE') return res.sendStatus(401)
|
|
133
|
+
throw err
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- Firma: `X-Signature: sha256=` + HMAC-SHA256(secreto, `timestamp + "." + cuerpo`), con `X-Signature-Timestamp` en segundos Unix. Se rechazan firmas con más de 300 s de antigüedad (`toleranceSeconds`).
|
|
139
|
+
- `verifyWebhookSignature(...)` devuelve `true`/`false` sin lanzar, si prefieres manejarlo tú.
|
|
140
|
+
- Cuerpo (`WebhookEvent`, versión `'1'`): `event` (`'extraction.completed'`), `idempotencyKey`, `documentType`, `source` (`'web'` | `'api'` | `'capture-link'`), `captureLinkId`, `reference`, `extractedAt`, `data` y, si el destino lo tiene activado, `image` (`front`/`back` en base64).
|
|
141
|
+
|
|
71
142
|
## Manejo de errores
|
|
72
143
|
|
|
73
144
|
Toda respuesta no exitosa lanza un `IneExtractorError` con un `code` legible por máquina.
|
|
@@ -113,6 +184,10 @@ try {
|
|
|
113
184
|
| `URL_INVALID` | 400 | URL inválida (debe ser HTTPS) |
|
|
114
185
|
| `URL_BLOCKED` | 400 | URL bloqueada por seguridad |
|
|
115
186
|
| `URL_TIMEOUT` | 400 | Timeout al descargar la imagen (10 s) |
|
|
187
|
+
| `DESTINATION_NOT_FOUND` | 400 | El destino no existe o está inactivo |
|
|
188
|
+
| `TOO_MANY_LIVE_LINKS` | 400 | Ya tienes 20 enlaces de captura vivos |
|
|
189
|
+
| `INVALID_NAME` / `INVALID_REFERENCE` / `INVALID_REQUESTER_NAME` / `INVALID_DOCUMENT_TYPE` | 400 | Campo inválido al crear un enlace de captura |
|
|
190
|
+
| `NOT_FOUND` | 404 | Enlaces de captura desactivados en tu cuenta |
|
|
116
191
|
| `UNSUPPORTED_CONTENT_TYPE` | 415 | Content-Type no soportado |
|
|
117
192
|
| `INSUFFICIENT_TOKENS` | 402 | Saldo insuficiente (ver `err.enrollUrl`) |
|
|
118
193
|
| `LOW_IMAGE_QUALITY` | 422 | Imagen ilegible (ver `err.missingFields`) |
|
|
@@ -121,6 +196,7 @@ try {
|
|
|
121
196
|
| `INTERNAL_ERROR` | 500 | Error interno del servidor |
|
|
122
197
|
| `NETWORK_ERROR` | — | Fallo de red (lado del cliente) |
|
|
123
198
|
| `TIMEOUT` | — | Se superó el `timeoutMs` del cliente |
|
|
199
|
+
| `INVALID_SIGNATURE` | — | Firma de webhook inválida o vencida (`parseWebhook`) |
|
|
124
200
|
|
|
125
201
|
## Campos extraídos (`IneData`)
|
|
126
202
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,13 +17,26 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
IDEMPOTENCY_KEY_HEADER: () => IDEMPOTENCY_KEY_HEADER,
|
|
23
34
|
IneExtractorClient: () => IneExtractorClient,
|
|
24
|
-
IneExtractorError: () => IneExtractorError
|
|
35
|
+
IneExtractorError: () => IneExtractorError,
|
|
36
|
+
SIGNATURE_HEADER: () => SIGNATURE_HEADER,
|
|
37
|
+
SIGNATURE_TIMESTAMP_HEADER: () => SIGNATURE_TIMESTAMP_HEADER,
|
|
38
|
+
parseWebhook: () => parseWebhook,
|
|
39
|
+
verifyWebhookSignature: () => verifyWebhookSignature
|
|
25
40
|
});
|
|
26
41
|
module.exports = __toCommonJS(index_exports);
|
|
27
42
|
|
|
@@ -141,13 +156,65 @@ var IneExtractorClient = class {
|
|
|
141
156
|
}
|
|
142
157
|
init = { method: "POST", body: form };
|
|
143
158
|
}
|
|
159
|
+
if (input.destinationId) {
|
|
160
|
+
init.headers = {
|
|
161
|
+
...init.headers,
|
|
162
|
+
"X-Destination-Id": input.destinationId
|
|
163
|
+
};
|
|
164
|
+
}
|
|
144
165
|
const body = await this.request("/extract", init);
|
|
145
|
-
|
|
166
|
+
const result = {
|
|
146
167
|
extractionId: body.extraction_id,
|
|
147
168
|
data: body.data,
|
|
148
169
|
tokensRemaining: body.tokens_remaining,
|
|
149
170
|
uploadMethod: body.upload_method
|
|
150
171
|
};
|
|
172
|
+
if (body.delivery) {
|
|
173
|
+
const d = body.delivery;
|
|
174
|
+
const delivery = {
|
|
175
|
+
destinationId: d.destination_id,
|
|
176
|
+
succeeded: d.succeeded,
|
|
177
|
+
httpStatus: d.http_status ?? null,
|
|
178
|
+
latencyMs: d.latency_ms ?? null,
|
|
179
|
+
errorCode: d.error_code ?? null
|
|
180
|
+
};
|
|
181
|
+
result.delivery = delivery;
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Crea un enlace de captura (BETA): una URL de un solo uso que envías a tu
|
|
187
|
+
* cliente para que fotografíe su INE o pasaporte desde el celular. Los datos
|
|
188
|
+
* llegan a tu destino (`destinationId`), no a esta respuesta.
|
|
189
|
+
*
|
|
190
|
+
* Crear el enlace no gasta tokens; el escaneo del invitado gasta uno y se
|
|
191
|
+
* reembolsa si la extracción o la entrega fallan.
|
|
192
|
+
*
|
|
193
|
+
* @throws {IneExtractorError} `DESTINATION_NOT_FOUND`, `TOO_MANY_LIVE_LINKS`,
|
|
194
|
+
* `INVALID_REFERENCE`, `NOT_FOUND` (enlaces desactivados en tu cuenta)…
|
|
195
|
+
*/
|
|
196
|
+
async createCaptureLink(input) {
|
|
197
|
+
if (!input || !input.destinationId) {
|
|
198
|
+
throw new IneExtractorError("Falta `destinationId`.", "INVALID_INPUT");
|
|
199
|
+
}
|
|
200
|
+
const payload = {
|
|
201
|
+
destinationId: input.destinationId,
|
|
202
|
+
documentType: input.documentType ?? "ine"
|
|
203
|
+
};
|
|
204
|
+
if (input.requireBack) payload.requireBack = true;
|
|
205
|
+
if (input.reference !== void 0) payload.reference = input.reference;
|
|
206
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
207
|
+
if (input.requesterName !== void 0) payload.requesterName = input.requesterName;
|
|
208
|
+
const body = await this.request("/capture-links", jsonRequest(payload));
|
|
209
|
+
return {
|
|
210
|
+
id: body.id,
|
|
211
|
+
url: body.url,
|
|
212
|
+
reference: body.reference ?? null,
|
|
213
|
+
documentType: body.documentType,
|
|
214
|
+
requireBack: body.requireBack,
|
|
215
|
+
askGuestPersona: body.askGuestPersona,
|
|
216
|
+
expiresAt: body.expiresAt
|
|
217
|
+
};
|
|
151
218
|
}
|
|
152
219
|
async request(path, init) {
|
|
153
220
|
const controller = new AbortController();
|
|
@@ -213,9 +280,67 @@ function jsonRequest(payload) {
|
|
|
213
280
|
body: JSON.stringify(payload)
|
|
214
281
|
};
|
|
215
282
|
}
|
|
283
|
+
|
|
284
|
+
// src/webhooks.ts
|
|
285
|
+
var SIGNATURE_HEADER = "X-Signature";
|
|
286
|
+
var SIGNATURE_TIMESTAMP_HEADER = "X-Signature-Timestamp";
|
|
287
|
+
var IDEMPOTENCY_KEY_HEADER = "X-Idempotency-Key";
|
|
288
|
+
var DEFAULT_TOLERANCE_S = 300;
|
|
289
|
+
async function verifyWebhookSignature(input) {
|
|
290
|
+
const { secret, signature, timestamp } = input;
|
|
291
|
+
if (!secret || !signature || !timestamp || !/^\d+$/.test(timestamp)) return false;
|
|
292
|
+
const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_S;
|
|
293
|
+
if (tolerance > 0) {
|
|
294
|
+
const now = input.now ?? Math.floor(Date.now() / 1e3);
|
|
295
|
+
if (Math.abs(now - Number(timestamp)) > tolerance) return false;
|
|
296
|
+
}
|
|
297
|
+
const body = typeof input.rawBody === "string" ? input.rawBody : new TextDecoder().decode(input.rawBody);
|
|
298
|
+
const expected = "sha256=" + await hmacSha256Hex(secret, `${timestamp}.${body}`);
|
|
299
|
+
return constantTimeEqual(expected, signature);
|
|
300
|
+
}
|
|
301
|
+
async function parseWebhook(input) {
|
|
302
|
+
if (!await verifyWebhookSignature(input)) {
|
|
303
|
+
throw new IneExtractorError("Firma de webhook inv\xE1lida o vencida.", "INVALID_SIGNATURE");
|
|
304
|
+
}
|
|
305
|
+
const body = typeof input.rawBody === "string" ? input.rawBody : new TextDecoder().decode(input.rawBody);
|
|
306
|
+
try {
|
|
307
|
+
return JSON.parse(body);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
throw new IneExtractorError("El cuerpo del webhook no es JSON.", "INVALID_INPUT", { cause: err });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function hmacSha256Hex(secret, message) {
|
|
313
|
+
const enc = new TextEncoder();
|
|
314
|
+
const subtle = await getSubtle();
|
|
315
|
+
const key = await subtle.importKey(
|
|
316
|
+
"raw",
|
|
317
|
+
enc.encode(secret),
|
|
318
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
319
|
+
false,
|
|
320
|
+
["sign"]
|
|
321
|
+
);
|
|
322
|
+
const sig = new Uint8Array(await subtle.sign("HMAC", key, enc.encode(message)));
|
|
323
|
+
return Array.from(sig, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
324
|
+
}
|
|
325
|
+
async function getSubtle() {
|
|
326
|
+
if (globalThis.crypto?.subtle) return globalThis.crypto.subtle;
|
|
327
|
+
const { webcrypto } = await import("crypto");
|
|
328
|
+
return webcrypto.subtle;
|
|
329
|
+
}
|
|
330
|
+
function constantTimeEqual(a, b) {
|
|
331
|
+
if (a.length !== b.length) return false;
|
|
332
|
+
let diff = 0;
|
|
333
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
334
|
+
return diff === 0;
|
|
335
|
+
}
|
|
216
336
|
// Annotate the CommonJS export names for ESM import in node:
|
|
217
337
|
0 && (module.exports = {
|
|
338
|
+
IDEMPOTENCY_KEY_HEADER,
|
|
218
339
|
IneExtractorClient,
|
|
219
|
-
IneExtractorError
|
|
340
|
+
IneExtractorError,
|
|
341
|
+
SIGNATURE_HEADER,
|
|
342
|
+
SIGNATURE_TIMESTAMP_HEADER,
|
|
343
|
+
parseWebhook,
|
|
344
|
+
verifyWebhookSignature
|
|
220
345
|
});
|
|
221
346
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\n * SDK oficial de Extraer Datos de INE.\n * API de OCR para credenciales INE/IFE mexicanas.\n *\n * @see https://extraerdatosdeine.com/docs\n * @packageDocumentation\n */\nexport { IneExtractorClient } from './client.js'\nexport { IneExtractorError } from './errors.js'\nexport type { IneErrorCode } from './errors.js'\nexport type {\n IneData,\n ExtractResult,\n ExtractInput,\n ImageSource,\n BinaryImage,\n IneExtractorOptions,\n} from './types.js'\n","/**\n * Códigos de error que devuelve la API de Extraer Datos de INE, más algunos\n * códigos propios del SDK (`NETWORK_ERROR`, `TIMEOUT`).\n *\n * @see https://extraerdatosdeine.com/docs\n */\nexport type IneErrorCode =\n // 401\n | 'MISSING_API_KEY'\n | 'INVALID_API_KEY'\n // 400\n | 'MISSING_IMAGE'\n | 'INVALID_IMAGE_FORMAT'\n | 'IMAGE_TOO_LARGE'\n | 'INVALID_BASE64'\n | 'INVALID_MULTIPART'\n | 'URL_FETCH_FAILED'\n | 'URL_INVALID'\n | 'URL_BLOCKED'\n | 'URL_TIMEOUT'\n // 415\n | 'UNSUPPORTED_CONTENT_TYPE'\n // 402\n | 'INSUFFICIENT_TOKENS'\n // 422\n | 'LOW_IMAGE_QUALITY'\n // 500\n | 'EXTRACTION_FAILED'\n | 'PROCESSING_ERROR'\n | 'INTERNAL_ERROR'\n // SDK-only\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'INVALID_INPUT'\n // forward-compatible: unknown server codes\n | (string & {})\n\n/**\n * Error lanzado por el SDK ante cualquier respuesta no exitosa de la API\n * o un fallo de red/timeout. Inspecciona `code` para reaccionar de forma\n * programática.\n *\n * @example\n * try {\n * await client.extract({ front })\n * } catch (err) {\n * if (err instanceof IneExtractorError && err.code === 'INSUFFICIENT_TOKENS') {\n * console.log('Recarga tokens en', err.enrollUrl)\n * }\n * }\n */\nexport class IneExtractorError extends Error {\n /** Código de error legible por máquina. */\n readonly code: IneErrorCode\n /** Status HTTP de la respuesta (ausente en errores de red). */\n readonly status?: number\n /** ID de la extracción, cuando aplica (LOW_IMAGE_QUALITY / EXTRACTION_FAILED). */\n readonly extractionId?: string\n /** Campos que no se pudieron leer (LOW_IMAGE_QUALITY). */\n readonly missingFields?: string[]\n /** URL para configurar auto-recarga (algunos INSUFFICIENT_TOKENS). */\n readonly enrollUrl?: string\n /** Motivo de bloqueo (algunos INSUFFICIENT_TOKENS). */\n readonly blockReason?: string\n /** Cuerpo crudo de la respuesta de error, por si necesitas más detalle. */\n readonly response?: unknown\n\n constructor(\n message: string,\n code: IneErrorCode,\n init: {\n status?: number\n extractionId?: string\n missingFields?: string[]\n enrollUrl?: string\n blockReason?: string\n response?: unknown\n cause?: unknown\n } = {}\n ) {\n super(message, init.cause !== undefined ? { cause: init.cause } : undefined)\n this.name = 'IneExtractorError'\n this.code = code\n this.status = init.status\n this.extractionId = init.extractionId\n this.missingFields = init.missingFields\n this.enrollUrl = init.enrollUrl\n this.blockReason = init.blockReason\n this.response = init.response\n // Restore prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, IneExtractorError.prototype)\n }\n\n /** Construye el error a partir de una respuesta de la API. */\n static fromResponse(status: number, body: unknown): IneExtractorError {\n const b = (body ?? {}) as Record<string, unknown>\n const code = (typeof b.code === 'string' ? b.code : 'INTERNAL_ERROR') as IneErrorCode\n const message =\n (typeof b.error === 'string' && b.error) ||\n `La API respondió con status ${status}`\n return new IneExtractorError(message, code, {\n status,\n extractionId: typeof b.extraction_id === 'string' ? b.extraction_id : undefined,\n missingFields: Array.isArray(b.missing_fields)\n ? (b.missing_fields as string[])\n : undefined,\n enrollUrl: typeof b.enroll_url === 'string' ? b.enroll_url : undefined,\n blockReason: typeof b.block_reason === 'string' ? b.block_reason : undefined,\n response: body,\n })\n }\n}\n","import { IneExtractorError } from './errors.js'\nimport type {\n BinaryImage,\n ExtractInput,\n ExtractResult,\n ImageSource,\n IneExtractorOptions,\n} from './types.js'\n\nconst DEFAULT_BASE_URL = 'https://extraerdatosdeine.com/api/v1'\nconst DEFAULT_TIMEOUT_MS = 60_000\n\n/**\n * Cliente para la API de Extraer Datos de INE.\n *\n * @example\n * import { readFile } from 'node:fs/promises'\n * import { IneExtractorClient } from 'extraer-datos-ine'\n *\n * const client = new IneExtractorClient({ apiKey: process.env.INE_API_KEY! })\n * const front = await readFile('./ine_frente.jpg')\n * const back = await readFile('./ine_reverso.jpg')\n * const { data } = await client.extract({ front, back })\n * console.log(data.curp, data.claveElector)\n *\n * @see https://extraerdatosdeine.com/docs\n */\nexport class IneExtractorClient {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly timeoutMs: number\n private readonly fetchImpl: typeof fetch\n\n constructor(options: IneExtractorOptions) {\n if (!options || !options.apiKey) {\n throw new IneExtractorError('Falta `apiKey` en las opciones del cliente.', 'INVALID_INPUT')\n }\n const fetchImpl = options.fetch ?? globalThis.fetch\n if (typeof fetchImpl !== 'function') {\n throw new IneExtractorError(\n 'No hay `fetch` disponible. Usa Node 18+ o pasa `fetch` en las opciones.',\n 'INVALID_INPUT'\n )\n }\n this.apiKey = options.apiKey\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n this.fetchImpl = fetchImpl\n }\n\n /**\n * Consulta el saldo de tokens de tu cuenta.\n * @returns número de tokens disponibles.\n */\n async getBalance(): Promise<number> {\n const body = await this.request('/balance', { method: 'GET' })\n return Number((body as { balance: number }).balance)\n }\n\n /**\n * Extrae los datos de una credencial INE/IFE.\n *\n * El método de envío se elige automáticamente según el tipo de `front`:\n * binario → multipart; `{ base64 }` o string → JSON base64; `{ url }` → JSON URL.\n * `front` y `back` deben ser del mismo tipo (la API no mezcla métodos).\n *\n * @throws {IneExtractorError} ante saldo insuficiente, imagen ilegible,\n * formato inválido, error de red, timeout, etc. Revisa `error.code`.\n */\n async extract(input: ExtractInput): Promise<ExtractResult> {\n if (!input || input.front === undefined || input.front === null) {\n throw new IneExtractorError('Falta la imagen frontal (`front`).', 'MISSING_IMAGE')\n }\n\n const frontKind = kindOf(input.front)\n if (input.back !== undefined && kindOf(input.back) !== frontKind) {\n throw new IneExtractorError(\n '`front` y `back` deben ser del mismo tipo (ambos binarios, base64 o url).',\n 'INVALID_INPUT'\n )\n }\n\n let init: RequestInit\n if (frontKind === 'url') {\n const payload: Record<string, string> = {\n image_front_url: (input.front as { url: string }).url,\n }\n if (input.back) payload.image_back_url = (input.back as { url: string }).url\n init = jsonRequest(payload)\n } else if (frontKind === 'base64') {\n const payload: Record<string, string> = { image_front: asBase64(input.front) }\n if (input.back) payload.image_back = asBase64(input.back)\n init = jsonRequest(payload)\n } else {\n const form = new FormData()\n form.append('image_front', toBlob(input.front as BinaryImage, input.frontMimeType))\n if (input.back) {\n form.append('image_back', toBlob(input.back as BinaryImage, input.backMimeType))\n }\n init = { method: 'POST', body: form }\n }\n\n const body = (await this.request('/extract', init)) as {\n extraction_id: string\n data: ExtractResult['data']\n tokens_remaining: number\n upload_method: string\n }\n\n return {\n extractionId: body.extraction_id,\n data: body.data,\n tokensRemaining: body.tokens_remaining,\n uploadMethod: body.upload_method,\n }\n }\n\n private async request(path: string, init: RequestInit): Promise<unknown> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\n\n let res: Response\n try {\n res = await this.fetchImpl(this.baseUrl + path, {\n ...init,\n signal: controller.signal,\n headers: { 'X-API-Key': this.apiKey, ...(init.headers ?? {}) },\n })\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new IneExtractorError(\n `La petición excedió el timeout de ${this.timeoutMs} ms.`,\n 'TIMEOUT',\n { cause: err }\n )\n }\n throw new IneExtractorError(\n err instanceof Error ? err.message : 'Error de red',\n 'NETWORK_ERROR',\n { cause: err }\n )\n } finally {\n clearTimeout(timer)\n }\n\n const body = await res.json().catch(() => ({}))\n if (!res.ok || (body as { success?: boolean })?.success === false) {\n throw IneExtractorError.fromResponse(res.status, body)\n }\n return body\n }\n}\n\ntype SourceKind = 'binary' | 'base64' | 'url'\n\nfunction kindOf(src: ImageSource): SourceKind {\n if (typeof src === 'string') return 'base64'\n if (src instanceof Uint8Array || src instanceof ArrayBuffer) return 'binary'\n if (src instanceof Blob) return 'binary'\n if (typeof src === 'object' && src !== null) {\n if ('url' in src) return 'url'\n if ('base64' in src) return 'base64'\n }\n throw new IneExtractorError(\n 'Fuente de imagen no soportada. Usa Uint8Array, ArrayBuffer, Blob, { base64 } o { url }.',\n 'INVALID_INPUT'\n )\n}\n\nfunction asBase64(src: ImageSource): string {\n if (typeof src === 'string') return src\n if (typeof src === 'object' && src !== null && 'base64' in src) return src.base64\n throw new IneExtractorError('Se esperaba una cadena base64.', 'INVALID_INPUT')\n}\n\nfunction toBlob(src: BinaryImage, mimeType = 'image/jpeg'): Blob {\n if (src instanceof Blob) return src\n if (src instanceof ArrayBuffer) return new Blob([src], { type: mimeType })\n // Uint8Array: copy exactly its bytes into a standalone ArrayBuffer so the\n // result is a valid BlobPart regardless of the backing buffer kind.\n const bytes = src.buffer.slice(src.byteOffset, src.byteOffset + src.byteLength)\n return new Blob([bytes as ArrayBuffer], { type: mimeType })\n}\n\nfunction jsonRequest(payload: Record<string, string>): RequestInit {\n return {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/client.ts","../src/webhooks.ts"],"sourcesContent":["/**\n * SDK oficial de Extraer Datos de INE.\n * API de OCR para credenciales INE/IFE mexicanas.\n *\n * @see https://extraerdatosdeine.com/docs\n * @packageDocumentation\n */\nexport { IneExtractorClient } from './client.js'\nexport { IneExtractorError } from './errors.js'\nexport type { IneErrorCode } from './errors.js'\nexport {\n verifyWebhookSignature,\n parseWebhook,\n SIGNATURE_HEADER,\n SIGNATURE_TIMESTAMP_HEADER,\n IDEMPOTENCY_KEY_HEADER,\n} from './webhooks.js'\nexport type { VerifyWebhookInput } from './webhooks.js'\nexport type {\n IneData,\n ExtractResult,\n ExtractInput,\n ImageSource,\n BinaryImage,\n IneExtractorOptions,\n DeliveryReport,\n CaptureDocumentType,\n CreateCaptureLinkInput,\n CaptureLink,\n WebhookEvent,\n} from './types.js'\n","/**\n * Códigos de error que devuelve la API de Extraer Datos de INE, más algunos\n * códigos propios del SDK (`NETWORK_ERROR`, `TIMEOUT`).\n *\n * @see https://extraerdatosdeine.com/docs\n */\nexport type IneErrorCode =\n // 401\n | 'MISSING_API_KEY'\n | 'INVALID_API_KEY'\n // 400\n | 'MISSING_IMAGE'\n | 'INVALID_IMAGE_FORMAT'\n | 'IMAGE_TOO_LARGE'\n | 'INVALID_BASE64'\n | 'INVALID_MULTIPART'\n | 'URL_FETCH_FAILED'\n | 'URL_INVALID'\n | 'URL_BLOCKED'\n | 'URL_TIMEOUT'\n | 'DESTINATION_NOT_FOUND'\n | 'INVALID_NAME'\n | 'INVALID_REFERENCE'\n | 'INVALID_REQUESTER_NAME'\n | 'INVALID_DOCUMENT_TYPE'\n | 'TOO_MANY_LIVE_LINKS'\n // 404 (enlaces de captura desactivados en la cuenta)\n | 'NOT_FOUND'\n // 415\n | 'UNSUPPORTED_CONTENT_TYPE'\n // 402\n | 'INSUFFICIENT_TOKENS'\n // 422\n | 'LOW_IMAGE_QUALITY'\n // 500\n | 'EXTRACTION_FAILED'\n | 'PROCESSING_ERROR'\n | 'INTERNAL_ERROR'\n // SDK-only\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'INVALID_INPUT'\n | 'INVALID_SIGNATURE'\n // forward-compatible: unknown server codes\n | (string & {})\n\n/**\n * Error lanzado por el SDK ante cualquier respuesta no exitosa de la API\n * o un fallo de red/timeout. Inspecciona `code` para reaccionar de forma\n * programática.\n *\n * @example\n * try {\n * await client.extract({ front })\n * } catch (err) {\n * if (err instanceof IneExtractorError && err.code === 'INSUFFICIENT_TOKENS') {\n * console.log('Recarga tokens en', err.enrollUrl)\n * }\n * }\n */\nexport class IneExtractorError extends Error {\n /** Código de error legible por máquina. */\n readonly code: IneErrorCode\n /** Status HTTP de la respuesta (ausente en errores de red). */\n readonly status?: number\n /** ID de la extracción, cuando aplica (LOW_IMAGE_QUALITY / EXTRACTION_FAILED). */\n readonly extractionId?: string\n /** Campos que no se pudieron leer (LOW_IMAGE_QUALITY). */\n readonly missingFields?: string[]\n /** URL para configurar auto-recarga (algunos INSUFFICIENT_TOKENS). */\n readonly enrollUrl?: string\n /** Motivo de bloqueo (algunos INSUFFICIENT_TOKENS). */\n readonly blockReason?: string\n /** Cuerpo crudo de la respuesta de error, por si necesitas más detalle. */\n readonly response?: unknown\n\n constructor(\n message: string,\n code: IneErrorCode,\n init: {\n status?: number\n extractionId?: string\n missingFields?: string[]\n enrollUrl?: string\n blockReason?: string\n response?: unknown\n cause?: unknown\n } = {}\n ) {\n super(message, init.cause !== undefined ? { cause: init.cause } : undefined)\n this.name = 'IneExtractorError'\n this.code = code\n this.status = init.status\n this.extractionId = init.extractionId\n this.missingFields = init.missingFields\n this.enrollUrl = init.enrollUrl\n this.blockReason = init.blockReason\n this.response = init.response\n // Restore prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, IneExtractorError.prototype)\n }\n\n /** Construye el error a partir de una respuesta de la API. */\n static fromResponse(status: number, body: unknown): IneExtractorError {\n const b = (body ?? {}) as Record<string, unknown>\n const code = (typeof b.code === 'string' ? b.code : 'INTERNAL_ERROR') as IneErrorCode\n const message =\n (typeof b.error === 'string' && b.error) ||\n `La API respondió con status ${status}`\n return new IneExtractorError(message, code, {\n status,\n extractionId: typeof b.extraction_id === 'string' ? b.extraction_id : undefined,\n missingFields: Array.isArray(b.missing_fields)\n ? (b.missing_fields as string[])\n : undefined,\n enrollUrl: typeof b.enroll_url === 'string' ? b.enroll_url : undefined,\n blockReason: typeof b.block_reason === 'string' ? b.block_reason : undefined,\n response: body,\n })\n }\n}\n","import { IneExtractorError } from './errors.js'\nimport type {\n BinaryImage,\n CaptureLink,\n CreateCaptureLinkInput,\n DeliveryReport,\n ExtractInput,\n ExtractResult,\n ImageSource,\n IneExtractorOptions,\n} from './types.js'\n\nconst DEFAULT_BASE_URL = 'https://extraerdatosdeine.com/api/v1'\nconst DEFAULT_TIMEOUT_MS = 60_000\n\n/**\n * Cliente para la API de Extraer Datos de INE.\n *\n * @example\n * import { readFile } from 'node:fs/promises'\n * import { IneExtractorClient } from 'extraer-datos-ine'\n *\n * const client = new IneExtractorClient({ apiKey: process.env.INE_API_KEY! })\n * const front = await readFile('./ine_frente.jpg')\n * const back = await readFile('./ine_reverso.jpg')\n * const { data } = await client.extract({ front, back })\n * console.log(data.curp, data.claveElector)\n *\n * @see https://extraerdatosdeine.com/docs\n */\nexport class IneExtractorClient {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly timeoutMs: number\n private readonly fetchImpl: typeof fetch\n\n constructor(options: IneExtractorOptions) {\n if (!options || !options.apiKey) {\n throw new IneExtractorError('Falta `apiKey` en las opciones del cliente.', 'INVALID_INPUT')\n }\n const fetchImpl = options.fetch ?? globalThis.fetch\n if (typeof fetchImpl !== 'function') {\n throw new IneExtractorError(\n 'No hay `fetch` disponible. Usa Node 18+ o pasa `fetch` en las opciones.',\n 'INVALID_INPUT'\n )\n }\n this.apiKey = options.apiKey\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n this.fetchImpl = fetchImpl\n }\n\n /**\n * Consulta el saldo de tokens de tu cuenta.\n * @returns número de tokens disponibles.\n */\n async getBalance(): Promise<number> {\n const body = await this.request('/balance', { method: 'GET' })\n return Number((body as { balance: number }).balance)\n }\n\n /**\n * Extrae los datos de una credencial INE/IFE.\n *\n * El método de envío se elige automáticamente según el tipo de `front`:\n * binario → multipart; `{ base64 }` o string → JSON base64; `{ url }` → JSON URL.\n * `front` y `back` deben ser del mismo tipo (la API no mezcla métodos).\n *\n * @throws {IneExtractorError} ante saldo insuficiente, imagen ilegible,\n * formato inválido, error de red, timeout, etc. Revisa `error.code`.\n */\n async extract(input: ExtractInput): Promise<ExtractResult> {\n if (!input || input.front === undefined || input.front === null) {\n throw new IneExtractorError('Falta la imagen frontal (`front`).', 'MISSING_IMAGE')\n }\n\n const frontKind = kindOf(input.front)\n if (input.back !== undefined && kindOf(input.back) !== frontKind) {\n throw new IneExtractorError(\n '`front` y `back` deben ser del mismo tipo (ambos binarios, base64 o url).',\n 'INVALID_INPUT'\n )\n }\n\n let init: RequestInit\n if (frontKind === 'url') {\n const payload: Record<string, string> = {\n image_front_url: (input.front as { url: string }).url,\n }\n if (input.back) payload.image_back_url = (input.back as { url: string }).url\n init = jsonRequest(payload)\n } else if (frontKind === 'base64') {\n const payload: Record<string, string> = { image_front: asBase64(input.front) }\n if (input.back) payload.image_back = asBase64(input.back)\n init = jsonRequest(payload)\n } else {\n const form = new FormData()\n form.append('image_front', toBlob(input.front as BinaryImage, input.frontMimeType))\n if (input.back) {\n form.append('image_back', toBlob(input.back as BinaryImage, input.backMimeType))\n }\n init = { method: 'POST', body: form }\n }\n\n if (input.destinationId) {\n init.headers = {\n ...(init.headers as Record<string, string> | undefined),\n 'X-Destination-Id': input.destinationId,\n }\n }\n\n const body = (await this.request('/extract', init)) as {\n extraction_id: string\n data: ExtractResult['data']\n tokens_remaining: number\n upload_method: string\n delivery?: {\n destination_id: string\n succeeded: boolean\n http_status: number | null\n latency_ms: number | null\n error_code: string | null\n }\n }\n\n const result: ExtractResult = {\n extractionId: body.extraction_id,\n data: body.data,\n tokensRemaining: body.tokens_remaining,\n uploadMethod: body.upload_method,\n }\n if (body.delivery) {\n const d = body.delivery\n const delivery: DeliveryReport = {\n destinationId: d.destination_id,\n succeeded: d.succeeded,\n httpStatus: d.http_status ?? null,\n latencyMs: d.latency_ms ?? null,\n errorCode: d.error_code ?? null,\n }\n result.delivery = delivery\n }\n return result\n }\n\n /**\n * Crea un enlace de captura (BETA): una URL de un solo uso que envías a tu\n * cliente para que fotografíe su INE o pasaporte desde el celular. Los datos\n * llegan a tu destino (`destinationId`), no a esta respuesta.\n *\n * Crear el enlace no gasta tokens; el escaneo del invitado gasta uno y se\n * reembolsa si la extracción o la entrega fallan.\n *\n * @throws {IneExtractorError} `DESTINATION_NOT_FOUND`, `TOO_MANY_LIVE_LINKS`,\n * `INVALID_REFERENCE`, `NOT_FOUND` (enlaces desactivados en tu cuenta)…\n */\n async createCaptureLink(input: CreateCaptureLinkInput): Promise<CaptureLink> {\n if (!input || !input.destinationId) {\n throw new IneExtractorError('Falta `destinationId`.', 'INVALID_INPUT')\n }\n const payload: Record<string, unknown> = {\n destinationId: input.destinationId,\n documentType: input.documentType ?? 'ine',\n }\n if (input.requireBack) payload.requireBack = true\n if (input.reference !== undefined) payload.reference = input.reference\n if (input.name !== undefined) payload.name = input.name\n if (input.requesterName !== undefined) payload.requesterName = input.requesterName\n\n const body = (await this.request('/capture-links', jsonRequest(payload))) as CaptureLink\n return {\n id: body.id,\n url: body.url,\n reference: body.reference ?? null,\n documentType: body.documentType,\n requireBack: body.requireBack,\n askGuestPersona: body.askGuestPersona,\n expiresAt: body.expiresAt,\n }\n }\n\n private async request(path: string, init: RequestInit): Promise<unknown> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\n\n let res: Response\n try {\n res = await this.fetchImpl(this.baseUrl + path, {\n ...init,\n signal: controller.signal,\n headers: { 'X-API-Key': this.apiKey, ...(init.headers ?? {}) },\n })\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new IneExtractorError(\n `La petición excedió el timeout de ${this.timeoutMs} ms.`,\n 'TIMEOUT',\n { cause: err }\n )\n }\n throw new IneExtractorError(\n err instanceof Error ? err.message : 'Error de red',\n 'NETWORK_ERROR',\n { cause: err }\n )\n } finally {\n clearTimeout(timer)\n }\n\n const body = await res.json().catch(() => ({}))\n if (!res.ok || (body as { success?: boolean })?.success === false) {\n throw IneExtractorError.fromResponse(res.status, body)\n }\n return body\n }\n}\n\ntype SourceKind = 'binary' | 'base64' | 'url'\n\nfunction kindOf(src: ImageSource): SourceKind {\n if (typeof src === 'string') return 'base64'\n if (src instanceof Uint8Array || src instanceof ArrayBuffer) return 'binary'\n if (src instanceof Blob) return 'binary'\n if (typeof src === 'object' && src !== null) {\n if ('url' in src) return 'url'\n if ('base64' in src) return 'base64'\n }\n throw new IneExtractorError(\n 'Fuente de imagen no soportada. Usa Uint8Array, ArrayBuffer, Blob, { base64 } o { url }.',\n 'INVALID_INPUT'\n )\n}\n\nfunction asBase64(src: ImageSource): string {\n if (typeof src === 'string') return src\n if (typeof src === 'object' && src !== null && 'base64' in src) return src.base64\n throw new IneExtractorError('Se esperaba una cadena base64.', 'INVALID_INPUT')\n}\n\nfunction toBlob(src: BinaryImage, mimeType = 'image/jpeg'): Blob {\n if (src instanceof Blob) return src\n if (src instanceof ArrayBuffer) return new Blob([src], { type: mimeType })\n // Uint8Array: copy exactly its bytes into a standalone ArrayBuffer so the\n // result is a valid BlobPart regardless of the backing buffer kind.\n const bytes = src.buffer.slice(src.byteOffset, src.byteOffset + src.byteLength)\n return new Blob([bytes as ArrayBuffer], { type: mimeType })\n}\n\nfunction jsonRequest(payload: Record<string, unknown>): RequestInit {\n return {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }\n}\n","import { IneExtractorError } from './errors.js'\nimport type { WebhookEvent } from './types.js'\n\n/** Header con `sha256=<hex>`. */\nexport const SIGNATURE_HEADER = 'X-Signature'\n/** Header con los segundos Unix que cubre la firma. */\nexport const SIGNATURE_TIMESTAMP_HEADER = 'X-Signature-Timestamp'\n/** Header con la clave de idempotencia de la entrega. */\nexport const IDEMPOTENCY_KEY_HEADER = 'X-Idempotency-Key'\n\nconst DEFAULT_TOLERANCE_S = 300\n\nexport interface VerifyWebhookInput {\n /** El secreto del destino (`whsec_…`), mostrado una vez al crearlo. */\n secret: string\n /** El cuerpo CRUDO, exactamente como llegó. No lo re-serialices. */\n rawBody: string | Uint8Array\n /** Valor del header `X-Signature`. */\n signature: string | null | undefined\n /** Valor del header `X-Signature-Timestamp`. */\n timestamp: string | null | undefined\n /**\n * Antigüedad máxima aceptada, en segundos (por defecto 300). Protege contra\n * la repetición de una entrega capturada. `0` desactiva la comprobación.\n */\n toleranceSeconds?: number\n /** Solo para pruebas: segundos Unix \"actuales\". */\n now?: number\n}\n\n/**\n * Comprueba que una entrega a tu webhook viene de Extraer Datos de INE.\n *\n * La firma es `sha256=` + HMAC-SHA256(secreto, `timestamp + \".\" + cuerpo`).\n * Devuelve false (nunca lanza) ante una firma ausente, malformada, incorrecta\n * o fuera de tiempo.\n */\nexport async function verifyWebhookSignature(input: VerifyWebhookInput): Promise<boolean> {\n const { secret, signature, timestamp } = input\n if (!secret || !signature || !timestamp || !/^\\d+$/.test(timestamp)) return false\n\n const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_S\n if (tolerance > 0) {\n const now = input.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - Number(timestamp)) > tolerance) return false\n }\n\n const body =\n typeof input.rawBody === 'string' ? input.rawBody : new TextDecoder().decode(input.rawBody)\n const expected = 'sha256=' + (await hmacSha256Hex(secret, `${timestamp}.${body}`))\n return constantTimeEqual(expected, signature)\n}\n\n/**\n * Verifica la firma y devuelve el cuerpo ya interpretado.\n *\n * @throws {IneExtractorError} `INVALID_SIGNATURE` si la firma no es válida.\n *\n * @example\n * // Express: usa express.raw({ type: 'application/json' }) en esta ruta.\n * const event = await parseWebhook({\n * secret: process.env.INE_WEBHOOK_SECRET!,\n * rawBody: req.body,\n * signature: req.get('X-Signature'),\n * timestamp: req.get('X-Signature-Timestamp'),\n * })\n * if (event.event === 'extraction.completed') console.log(event.reference, event.data.curp)\n */\nexport async function parseWebhook(input: VerifyWebhookInput): Promise<WebhookEvent> {\n if (!(await verifyWebhookSignature(input))) {\n throw new IneExtractorError('Firma de webhook inválida o vencida.', 'INVALID_SIGNATURE')\n }\n const body =\n typeof input.rawBody === 'string' ? input.rawBody : new TextDecoder().decode(input.rawBody)\n try {\n return JSON.parse(body) as WebhookEvent\n } catch (err) {\n throw new IneExtractorError('El cuerpo del webhook no es JSON.', 'INVALID_INPUT', { cause: err })\n }\n}\n\nasync function hmacSha256Hex(secret: string, message: string): Promise<string> {\n const enc = new TextEncoder()\n const subtle = await getSubtle()\n const key = await subtle.importKey(\n 'raw',\n enc.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n )\n const sig = new Uint8Array(await subtle.sign('HMAC', key, enc.encode(message)))\n return Array.from(sig, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\n/** Web Crypto: global en Node 19+, Deno, Bun, Workers; en Node 18 viene de node:crypto. */\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (globalThis.crypto?.subtle) return globalThis.crypto.subtle\n const { webcrypto } = await import('node:crypto')\n return webcrypto.subtle as SubtleCrypto\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -57,6 +57,24 @@ interface ExtractResult {
|
|
|
57
57
|
tokensRemaining: number;
|
|
58
58
|
/** Método de upload detectado por la API. */
|
|
59
59
|
uploadMethod: string;
|
|
60
|
+
/** Resultado de la entrega, solo si enviaste `destinationId`. */
|
|
61
|
+
delivery?: DeliveryReport;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resultado de entregar los datos a uno de tus destinos (webhook, Telegram…).
|
|
65
|
+
* Una entrega fallida NO hace fallar la extracción: revisa `succeeded`.
|
|
66
|
+
*/
|
|
67
|
+
interface DeliveryReport {
|
|
68
|
+
/** El destino al que se entregó. */
|
|
69
|
+
destinationId: string;
|
|
70
|
+
/** `true` si el destino aceptó la entrega. */
|
|
71
|
+
succeeded: boolean;
|
|
72
|
+
/** Status HTTP que respondió el destino, si hubo respuesta. */
|
|
73
|
+
httpStatus: number | null;
|
|
74
|
+
/** Duración de la entrega en ms. */
|
|
75
|
+
latencyMs: number | null;
|
|
76
|
+
/** Motivo del fallo (`http-error`, `timeout`, `blocked-target`…); `null` si tuvo éxito. */
|
|
77
|
+
errorCode: string | null;
|
|
60
78
|
}
|
|
61
79
|
/** Imagen binaria aceptada por el SDK (Node 18+ o navegador). */
|
|
62
80
|
type BinaryImage = Uint8Array | ArrayBuffer | Blob;
|
|
@@ -82,6 +100,45 @@ interface ExtractInput {
|
|
|
82
100
|
frontMimeType?: string;
|
|
83
101
|
/** MIME de la imagen trasera cuando es binaria. Default `image/jpeg`. */
|
|
84
102
|
backMimeType?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Entrega también los datos a uno de tus destinos (header `X-Destination-Id`).
|
|
105
|
+
* Si no existe o está inactivo, la llamada falla con `DESTINATION_NOT_FOUND`
|
|
106
|
+
* antes de gastar un token. El resultado llega en `result.delivery`.
|
|
107
|
+
*/
|
|
108
|
+
destinationId?: string;
|
|
109
|
+
}
|
|
110
|
+
/** Tipo de documento que pide un enlace de captura. */
|
|
111
|
+
type CaptureDocumentType = 'ine' | 'passport';
|
|
112
|
+
/** Parámetros de `client.createCaptureLink()`. */
|
|
113
|
+
interface CreateCaptureLinkInput {
|
|
114
|
+
/** Destino al que llegarán los datos del invitado (requerido). */
|
|
115
|
+
destinationId: string;
|
|
116
|
+
/** Documento que se pide. Default `'ine'`. */
|
|
117
|
+
documentType?: CaptureDocumentType;
|
|
118
|
+
/** Pide también el reverso. Default `false`. */
|
|
119
|
+
requireBack?: boolean;
|
|
120
|
+
/** Tu referencia (folio, habitación, expediente), máx. 80 caracteres. Viaja en el webhook. */
|
|
121
|
+
reference?: string;
|
|
122
|
+
/** Nombre del enlace en tu panel, máx. 60 caracteres. */
|
|
123
|
+
name?: string;
|
|
124
|
+
/** Nombre de tu negocio que ve el invitado, máx. 60 caracteres. */
|
|
125
|
+
requesterName?: string;
|
|
126
|
+
}
|
|
127
|
+
/** Enlace de captura recién creado. */
|
|
128
|
+
interface CaptureLink {
|
|
129
|
+
/** ID del enlace. */
|
|
130
|
+
id: string;
|
|
131
|
+
/**
|
|
132
|
+
* URL para el invitado. Contiene un token de acceso y solo se devuelve UNA
|
|
133
|
+
* vez: trátala como una credencial y no la registres en logs.
|
|
134
|
+
*/
|
|
135
|
+
url: string;
|
|
136
|
+
reference: string | null;
|
|
137
|
+
documentType: CaptureDocumentType;
|
|
138
|
+
requireBack: boolean;
|
|
139
|
+
askGuestPersona: boolean;
|
|
140
|
+
/** Fecha límite (ISO 8601) para que el invitado abra el enlace. */
|
|
141
|
+
expiresAt: string;
|
|
85
142
|
}
|
|
86
143
|
/** Opciones del constructor del cliente. */
|
|
87
144
|
interface IneExtractorOptions {
|
|
@@ -94,6 +151,32 @@ interface IneExtractorOptions {
|
|
|
94
151
|
/** Implementación de `fetch` a usar (para tests o entornos sin fetch global). */
|
|
95
152
|
fetch?: typeof fetch;
|
|
96
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Cuerpo que recibe tu webhook (versión `'1'`), tanto de `extract()` con
|
|
156
|
+
* `destinationId` como del escaneo de un enlace de captura.
|
|
157
|
+
*/
|
|
158
|
+
interface WebhookEvent {
|
|
159
|
+
version: '1';
|
|
160
|
+
event: 'extraction.completed' | (string & {});
|
|
161
|
+
/** Igual en cada reintento de la misma entrega (header `X-Idempotency-Key`). */
|
|
162
|
+
idempotencyKey: string;
|
|
163
|
+
documentType: CaptureDocumentType;
|
|
164
|
+
/** `'capture-link'` cuando viene del escaneo de un invitado. */
|
|
165
|
+
source: 'web' | 'api' | 'capture-link';
|
|
166
|
+
/** ID del enlace de captura, si `source` es `'capture-link'`. */
|
|
167
|
+
captureLinkId: string | null;
|
|
168
|
+
/** La `reference` que pusiste al crear el enlace. */
|
|
169
|
+
reference: string | null;
|
|
170
|
+
/** ISO 8601. */
|
|
171
|
+
extractedAt: string;
|
|
172
|
+
/** Campos extraídos (ver `IneData` para INE). */
|
|
173
|
+
data: Record<string, string | undefined>;
|
|
174
|
+
/** Imágenes en base64, solo si el destino tiene activado "incluir imagen". */
|
|
175
|
+
image?: {
|
|
176
|
+
front?: string;
|
|
177
|
+
back?: string;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
97
180
|
|
|
98
181
|
/**
|
|
99
182
|
* Cliente para la API de Extraer Datos de INE.
|
|
@@ -132,6 +215,18 @@ declare class IneExtractorClient {
|
|
|
132
215
|
* formato inválido, error de red, timeout, etc. Revisa `error.code`.
|
|
133
216
|
*/
|
|
134
217
|
extract(input: ExtractInput): Promise<ExtractResult>;
|
|
218
|
+
/**
|
|
219
|
+
* Crea un enlace de captura (BETA): una URL de un solo uso que envías a tu
|
|
220
|
+
* cliente para que fotografíe su INE o pasaporte desde el celular. Los datos
|
|
221
|
+
* llegan a tu destino (`destinationId`), no a esta respuesta.
|
|
222
|
+
*
|
|
223
|
+
* Crear el enlace no gasta tokens; el escaneo del invitado gasta uno y se
|
|
224
|
+
* reembolsa si la extracción o la entrega fallan.
|
|
225
|
+
*
|
|
226
|
+
* @throws {IneExtractorError} `DESTINATION_NOT_FOUND`, `TOO_MANY_LIVE_LINKS`,
|
|
227
|
+
* `INVALID_REFERENCE`, `NOT_FOUND` (enlaces desactivados en tu cuenta)…
|
|
228
|
+
*/
|
|
229
|
+
createCaptureLink(input: CreateCaptureLinkInput): Promise<CaptureLink>;
|
|
135
230
|
private request;
|
|
136
231
|
}
|
|
137
232
|
|
|
@@ -141,7 +236,7 @@ declare class IneExtractorClient {
|
|
|
141
236
|
*
|
|
142
237
|
* @see https://extraerdatosdeine.com/docs
|
|
143
238
|
*/
|
|
144
|
-
type IneErrorCode = 'MISSING_API_KEY' | 'INVALID_API_KEY' | 'MISSING_IMAGE' | 'INVALID_IMAGE_FORMAT' | 'IMAGE_TOO_LARGE' | 'INVALID_BASE64' | 'INVALID_MULTIPART' | 'URL_FETCH_FAILED' | 'URL_INVALID' | 'URL_BLOCKED' | 'URL_TIMEOUT' | 'UNSUPPORTED_CONTENT_TYPE' | 'INSUFFICIENT_TOKENS' | 'LOW_IMAGE_QUALITY' | 'EXTRACTION_FAILED' | 'PROCESSING_ERROR' | 'INTERNAL_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'INVALID_INPUT' | (string & {});
|
|
239
|
+
type IneErrorCode = 'MISSING_API_KEY' | 'INVALID_API_KEY' | 'MISSING_IMAGE' | 'INVALID_IMAGE_FORMAT' | 'IMAGE_TOO_LARGE' | 'INVALID_BASE64' | 'INVALID_MULTIPART' | 'URL_FETCH_FAILED' | 'URL_INVALID' | 'URL_BLOCKED' | 'URL_TIMEOUT' | 'DESTINATION_NOT_FOUND' | 'INVALID_NAME' | 'INVALID_REFERENCE' | 'INVALID_REQUESTER_NAME' | 'INVALID_DOCUMENT_TYPE' | 'TOO_MANY_LIVE_LINKS' | 'NOT_FOUND' | 'UNSUPPORTED_CONTENT_TYPE' | 'INSUFFICIENT_TOKENS' | 'LOW_IMAGE_QUALITY' | 'EXTRACTION_FAILED' | 'PROCESSING_ERROR' | 'INTERNAL_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'INVALID_INPUT' | 'INVALID_SIGNATURE' | (string & {});
|
|
145
240
|
/**
|
|
146
241
|
* Error lanzado por el SDK ante cualquier respuesta no exitosa de la API
|
|
147
242
|
* o un fallo de red/timeout. Inspecciona `code` para reaccionar de forma
|
|
@@ -184,4 +279,52 @@ declare class IneExtractorError extends Error {
|
|
|
184
279
|
static fromResponse(status: number, body: unknown): IneExtractorError;
|
|
185
280
|
}
|
|
186
281
|
|
|
187
|
-
|
|
282
|
+
/** Header con `sha256=<hex>`. */
|
|
283
|
+
declare const SIGNATURE_HEADER = "X-Signature";
|
|
284
|
+
/** Header con los segundos Unix que cubre la firma. */
|
|
285
|
+
declare const SIGNATURE_TIMESTAMP_HEADER = "X-Signature-Timestamp";
|
|
286
|
+
/** Header con la clave de idempotencia de la entrega. */
|
|
287
|
+
declare const IDEMPOTENCY_KEY_HEADER = "X-Idempotency-Key";
|
|
288
|
+
interface VerifyWebhookInput {
|
|
289
|
+
/** El secreto del destino (`whsec_…`), mostrado una vez al crearlo. */
|
|
290
|
+
secret: string;
|
|
291
|
+
/** El cuerpo CRUDO, exactamente como llegó. No lo re-serialices. */
|
|
292
|
+
rawBody: string | Uint8Array;
|
|
293
|
+
/** Valor del header `X-Signature`. */
|
|
294
|
+
signature: string | null | undefined;
|
|
295
|
+
/** Valor del header `X-Signature-Timestamp`. */
|
|
296
|
+
timestamp: string | null | undefined;
|
|
297
|
+
/**
|
|
298
|
+
* Antigüedad máxima aceptada, en segundos (por defecto 300). Protege contra
|
|
299
|
+
* la repetición de una entrega capturada. `0` desactiva la comprobación.
|
|
300
|
+
*/
|
|
301
|
+
toleranceSeconds?: number;
|
|
302
|
+
/** Solo para pruebas: segundos Unix "actuales". */
|
|
303
|
+
now?: number;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Comprueba que una entrega a tu webhook viene de Extraer Datos de INE.
|
|
307
|
+
*
|
|
308
|
+
* La firma es `sha256=` + HMAC-SHA256(secreto, `timestamp + "." + cuerpo`).
|
|
309
|
+
* Devuelve false (nunca lanza) ante una firma ausente, malformada, incorrecta
|
|
310
|
+
* o fuera de tiempo.
|
|
311
|
+
*/
|
|
312
|
+
declare function verifyWebhookSignature(input: VerifyWebhookInput): Promise<boolean>;
|
|
313
|
+
/**
|
|
314
|
+
* Verifica la firma y devuelve el cuerpo ya interpretado.
|
|
315
|
+
*
|
|
316
|
+
* @throws {IneExtractorError} `INVALID_SIGNATURE` si la firma no es válida.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* // Express: usa express.raw({ type: 'application/json' }) en esta ruta.
|
|
320
|
+
* const event = await parseWebhook({
|
|
321
|
+
* secret: process.env.INE_WEBHOOK_SECRET!,
|
|
322
|
+
* rawBody: req.body,
|
|
323
|
+
* signature: req.get('X-Signature'),
|
|
324
|
+
* timestamp: req.get('X-Signature-Timestamp'),
|
|
325
|
+
* })
|
|
326
|
+
* if (event.event === 'extraction.completed') console.log(event.reference, event.data.curp)
|
|
327
|
+
*/
|
|
328
|
+
declare function parseWebhook(input: VerifyWebhookInput): Promise<WebhookEvent>;
|
|
329
|
+
|
|
330
|
+
export { type BinaryImage, type CaptureDocumentType, type CaptureLink, type CreateCaptureLinkInput, type DeliveryReport, type ExtractInput, type ExtractResult, IDEMPOTENCY_KEY_HEADER, type ImageSource, type IneData, type IneErrorCode, IneExtractorClient, IneExtractorError, type IneExtractorOptions, SIGNATURE_HEADER, SIGNATURE_TIMESTAMP_HEADER, type VerifyWebhookInput, type WebhookEvent, parseWebhook, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,24 @@ interface ExtractResult {
|
|
|
57
57
|
tokensRemaining: number;
|
|
58
58
|
/** Método de upload detectado por la API. */
|
|
59
59
|
uploadMethod: string;
|
|
60
|
+
/** Resultado de la entrega, solo si enviaste `destinationId`. */
|
|
61
|
+
delivery?: DeliveryReport;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resultado de entregar los datos a uno de tus destinos (webhook, Telegram…).
|
|
65
|
+
* Una entrega fallida NO hace fallar la extracción: revisa `succeeded`.
|
|
66
|
+
*/
|
|
67
|
+
interface DeliveryReport {
|
|
68
|
+
/** El destino al que se entregó. */
|
|
69
|
+
destinationId: string;
|
|
70
|
+
/** `true` si el destino aceptó la entrega. */
|
|
71
|
+
succeeded: boolean;
|
|
72
|
+
/** Status HTTP que respondió el destino, si hubo respuesta. */
|
|
73
|
+
httpStatus: number | null;
|
|
74
|
+
/** Duración de la entrega en ms. */
|
|
75
|
+
latencyMs: number | null;
|
|
76
|
+
/** Motivo del fallo (`http-error`, `timeout`, `blocked-target`…); `null` si tuvo éxito. */
|
|
77
|
+
errorCode: string | null;
|
|
60
78
|
}
|
|
61
79
|
/** Imagen binaria aceptada por el SDK (Node 18+ o navegador). */
|
|
62
80
|
type BinaryImage = Uint8Array | ArrayBuffer | Blob;
|
|
@@ -82,6 +100,45 @@ interface ExtractInput {
|
|
|
82
100
|
frontMimeType?: string;
|
|
83
101
|
/** MIME de la imagen trasera cuando es binaria. Default `image/jpeg`. */
|
|
84
102
|
backMimeType?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Entrega también los datos a uno de tus destinos (header `X-Destination-Id`).
|
|
105
|
+
* Si no existe o está inactivo, la llamada falla con `DESTINATION_NOT_FOUND`
|
|
106
|
+
* antes de gastar un token. El resultado llega en `result.delivery`.
|
|
107
|
+
*/
|
|
108
|
+
destinationId?: string;
|
|
109
|
+
}
|
|
110
|
+
/** Tipo de documento que pide un enlace de captura. */
|
|
111
|
+
type CaptureDocumentType = 'ine' | 'passport';
|
|
112
|
+
/** Parámetros de `client.createCaptureLink()`. */
|
|
113
|
+
interface CreateCaptureLinkInput {
|
|
114
|
+
/** Destino al que llegarán los datos del invitado (requerido). */
|
|
115
|
+
destinationId: string;
|
|
116
|
+
/** Documento que se pide. Default `'ine'`. */
|
|
117
|
+
documentType?: CaptureDocumentType;
|
|
118
|
+
/** Pide también el reverso. Default `false`. */
|
|
119
|
+
requireBack?: boolean;
|
|
120
|
+
/** Tu referencia (folio, habitación, expediente), máx. 80 caracteres. Viaja en el webhook. */
|
|
121
|
+
reference?: string;
|
|
122
|
+
/** Nombre del enlace en tu panel, máx. 60 caracteres. */
|
|
123
|
+
name?: string;
|
|
124
|
+
/** Nombre de tu negocio que ve el invitado, máx. 60 caracteres. */
|
|
125
|
+
requesterName?: string;
|
|
126
|
+
}
|
|
127
|
+
/** Enlace de captura recién creado. */
|
|
128
|
+
interface CaptureLink {
|
|
129
|
+
/** ID del enlace. */
|
|
130
|
+
id: string;
|
|
131
|
+
/**
|
|
132
|
+
* URL para el invitado. Contiene un token de acceso y solo se devuelve UNA
|
|
133
|
+
* vez: trátala como una credencial y no la registres en logs.
|
|
134
|
+
*/
|
|
135
|
+
url: string;
|
|
136
|
+
reference: string | null;
|
|
137
|
+
documentType: CaptureDocumentType;
|
|
138
|
+
requireBack: boolean;
|
|
139
|
+
askGuestPersona: boolean;
|
|
140
|
+
/** Fecha límite (ISO 8601) para que el invitado abra el enlace. */
|
|
141
|
+
expiresAt: string;
|
|
85
142
|
}
|
|
86
143
|
/** Opciones del constructor del cliente. */
|
|
87
144
|
interface IneExtractorOptions {
|
|
@@ -94,6 +151,32 @@ interface IneExtractorOptions {
|
|
|
94
151
|
/** Implementación de `fetch` a usar (para tests o entornos sin fetch global). */
|
|
95
152
|
fetch?: typeof fetch;
|
|
96
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Cuerpo que recibe tu webhook (versión `'1'`), tanto de `extract()` con
|
|
156
|
+
* `destinationId` como del escaneo de un enlace de captura.
|
|
157
|
+
*/
|
|
158
|
+
interface WebhookEvent {
|
|
159
|
+
version: '1';
|
|
160
|
+
event: 'extraction.completed' | (string & {});
|
|
161
|
+
/** Igual en cada reintento de la misma entrega (header `X-Idempotency-Key`). */
|
|
162
|
+
idempotencyKey: string;
|
|
163
|
+
documentType: CaptureDocumentType;
|
|
164
|
+
/** `'capture-link'` cuando viene del escaneo de un invitado. */
|
|
165
|
+
source: 'web' | 'api' | 'capture-link';
|
|
166
|
+
/** ID del enlace de captura, si `source` es `'capture-link'`. */
|
|
167
|
+
captureLinkId: string | null;
|
|
168
|
+
/** La `reference` que pusiste al crear el enlace. */
|
|
169
|
+
reference: string | null;
|
|
170
|
+
/** ISO 8601. */
|
|
171
|
+
extractedAt: string;
|
|
172
|
+
/** Campos extraídos (ver `IneData` para INE). */
|
|
173
|
+
data: Record<string, string | undefined>;
|
|
174
|
+
/** Imágenes en base64, solo si el destino tiene activado "incluir imagen". */
|
|
175
|
+
image?: {
|
|
176
|
+
front?: string;
|
|
177
|
+
back?: string;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
97
180
|
|
|
98
181
|
/**
|
|
99
182
|
* Cliente para la API de Extraer Datos de INE.
|
|
@@ -132,6 +215,18 @@ declare class IneExtractorClient {
|
|
|
132
215
|
* formato inválido, error de red, timeout, etc. Revisa `error.code`.
|
|
133
216
|
*/
|
|
134
217
|
extract(input: ExtractInput): Promise<ExtractResult>;
|
|
218
|
+
/**
|
|
219
|
+
* Crea un enlace de captura (BETA): una URL de un solo uso que envías a tu
|
|
220
|
+
* cliente para que fotografíe su INE o pasaporte desde el celular. Los datos
|
|
221
|
+
* llegan a tu destino (`destinationId`), no a esta respuesta.
|
|
222
|
+
*
|
|
223
|
+
* Crear el enlace no gasta tokens; el escaneo del invitado gasta uno y se
|
|
224
|
+
* reembolsa si la extracción o la entrega fallan.
|
|
225
|
+
*
|
|
226
|
+
* @throws {IneExtractorError} `DESTINATION_NOT_FOUND`, `TOO_MANY_LIVE_LINKS`,
|
|
227
|
+
* `INVALID_REFERENCE`, `NOT_FOUND` (enlaces desactivados en tu cuenta)…
|
|
228
|
+
*/
|
|
229
|
+
createCaptureLink(input: CreateCaptureLinkInput): Promise<CaptureLink>;
|
|
135
230
|
private request;
|
|
136
231
|
}
|
|
137
232
|
|
|
@@ -141,7 +236,7 @@ declare class IneExtractorClient {
|
|
|
141
236
|
*
|
|
142
237
|
* @see https://extraerdatosdeine.com/docs
|
|
143
238
|
*/
|
|
144
|
-
type IneErrorCode = 'MISSING_API_KEY' | 'INVALID_API_KEY' | 'MISSING_IMAGE' | 'INVALID_IMAGE_FORMAT' | 'IMAGE_TOO_LARGE' | 'INVALID_BASE64' | 'INVALID_MULTIPART' | 'URL_FETCH_FAILED' | 'URL_INVALID' | 'URL_BLOCKED' | 'URL_TIMEOUT' | 'UNSUPPORTED_CONTENT_TYPE' | 'INSUFFICIENT_TOKENS' | 'LOW_IMAGE_QUALITY' | 'EXTRACTION_FAILED' | 'PROCESSING_ERROR' | 'INTERNAL_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'INVALID_INPUT' | (string & {});
|
|
239
|
+
type IneErrorCode = 'MISSING_API_KEY' | 'INVALID_API_KEY' | 'MISSING_IMAGE' | 'INVALID_IMAGE_FORMAT' | 'IMAGE_TOO_LARGE' | 'INVALID_BASE64' | 'INVALID_MULTIPART' | 'URL_FETCH_FAILED' | 'URL_INVALID' | 'URL_BLOCKED' | 'URL_TIMEOUT' | 'DESTINATION_NOT_FOUND' | 'INVALID_NAME' | 'INVALID_REFERENCE' | 'INVALID_REQUESTER_NAME' | 'INVALID_DOCUMENT_TYPE' | 'TOO_MANY_LIVE_LINKS' | 'NOT_FOUND' | 'UNSUPPORTED_CONTENT_TYPE' | 'INSUFFICIENT_TOKENS' | 'LOW_IMAGE_QUALITY' | 'EXTRACTION_FAILED' | 'PROCESSING_ERROR' | 'INTERNAL_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'INVALID_INPUT' | 'INVALID_SIGNATURE' | (string & {});
|
|
145
240
|
/**
|
|
146
241
|
* Error lanzado por el SDK ante cualquier respuesta no exitosa de la API
|
|
147
242
|
* o un fallo de red/timeout. Inspecciona `code` para reaccionar de forma
|
|
@@ -184,4 +279,52 @@ declare class IneExtractorError extends Error {
|
|
|
184
279
|
static fromResponse(status: number, body: unknown): IneExtractorError;
|
|
185
280
|
}
|
|
186
281
|
|
|
187
|
-
|
|
282
|
+
/** Header con `sha256=<hex>`. */
|
|
283
|
+
declare const SIGNATURE_HEADER = "X-Signature";
|
|
284
|
+
/** Header con los segundos Unix que cubre la firma. */
|
|
285
|
+
declare const SIGNATURE_TIMESTAMP_HEADER = "X-Signature-Timestamp";
|
|
286
|
+
/** Header con la clave de idempotencia de la entrega. */
|
|
287
|
+
declare const IDEMPOTENCY_KEY_HEADER = "X-Idempotency-Key";
|
|
288
|
+
interface VerifyWebhookInput {
|
|
289
|
+
/** El secreto del destino (`whsec_…`), mostrado una vez al crearlo. */
|
|
290
|
+
secret: string;
|
|
291
|
+
/** El cuerpo CRUDO, exactamente como llegó. No lo re-serialices. */
|
|
292
|
+
rawBody: string | Uint8Array;
|
|
293
|
+
/** Valor del header `X-Signature`. */
|
|
294
|
+
signature: string | null | undefined;
|
|
295
|
+
/** Valor del header `X-Signature-Timestamp`. */
|
|
296
|
+
timestamp: string | null | undefined;
|
|
297
|
+
/**
|
|
298
|
+
* Antigüedad máxima aceptada, en segundos (por defecto 300). Protege contra
|
|
299
|
+
* la repetición de una entrega capturada. `0` desactiva la comprobación.
|
|
300
|
+
*/
|
|
301
|
+
toleranceSeconds?: number;
|
|
302
|
+
/** Solo para pruebas: segundos Unix "actuales". */
|
|
303
|
+
now?: number;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Comprueba que una entrega a tu webhook viene de Extraer Datos de INE.
|
|
307
|
+
*
|
|
308
|
+
* La firma es `sha256=` + HMAC-SHA256(secreto, `timestamp + "." + cuerpo`).
|
|
309
|
+
* Devuelve false (nunca lanza) ante una firma ausente, malformada, incorrecta
|
|
310
|
+
* o fuera de tiempo.
|
|
311
|
+
*/
|
|
312
|
+
declare function verifyWebhookSignature(input: VerifyWebhookInput): Promise<boolean>;
|
|
313
|
+
/**
|
|
314
|
+
* Verifica la firma y devuelve el cuerpo ya interpretado.
|
|
315
|
+
*
|
|
316
|
+
* @throws {IneExtractorError} `INVALID_SIGNATURE` si la firma no es válida.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* // Express: usa express.raw({ type: 'application/json' }) en esta ruta.
|
|
320
|
+
* const event = await parseWebhook({
|
|
321
|
+
* secret: process.env.INE_WEBHOOK_SECRET!,
|
|
322
|
+
* rawBody: req.body,
|
|
323
|
+
* signature: req.get('X-Signature'),
|
|
324
|
+
* timestamp: req.get('X-Signature-Timestamp'),
|
|
325
|
+
* })
|
|
326
|
+
* if (event.event === 'extraction.completed') console.log(event.reference, event.data.curp)
|
|
327
|
+
*/
|
|
328
|
+
declare function parseWebhook(input: VerifyWebhookInput): Promise<WebhookEvent>;
|
|
329
|
+
|
|
330
|
+
export { type BinaryImage, type CaptureDocumentType, type CaptureLink, type CreateCaptureLinkInput, type DeliveryReport, type ExtractInput, type ExtractResult, IDEMPOTENCY_KEY_HEADER, type ImageSource, type IneData, type IneErrorCode, IneExtractorClient, IneExtractorError, type IneExtractorOptions, SIGNATURE_HEADER, SIGNATURE_TIMESTAMP_HEADER, type VerifyWebhookInput, type WebhookEvent, parseWebhook, verifyWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -114,13 +114,65 @@ var IneExtractorClient = class {
|
|
|
114
114
|
}
|
|
115
115
|
init = { method: "POST", body: form };
|
|
116
116
|
}
|
|
117
|
+
if (input.destinationId) {
|
|
118
|
+
init.headers = {
|
|
119
|
+
...init.headers,
|
|
120
|
+
"X-Destination-Id": input.destinationId
|
|
121
|
+
};
|
|
122
|
+
}
|
|
117
123
|
const body = await this.request("/extract", init);
|
|
118
|
-
|
|
124
|
+
const result = {
|
|
119
125
|
extractionId: body.extraction_id,
|
|
120
126
|
data: body.data,
|
|
121
127
|
tokensRemaining: body.tokens_remaining,
|
|
122
128
|
uploadMethod: body.upload_method
|
|
123
129
|
};
|
|
130
|
+
if (body.delivery) {
|
|
131
|
+
const d = body.delivery;
|
|
132
|
+
const delivery = {
|
|
133
|
+
destinationId: d.destination_id,
|
|
134
|
+
succeeded: d.succeeded,
|
|
135
|
+
httpStatus: d.http_status ?? null,
|
|
136
|
+
latencyMs: d.latency_ms ?? null,
|
|
137
|
+
errorCode: d.error_code ?? null
|
|
138
|
+
};
|
|
139
|
+
result.delivery = delivery;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Crea un enlace de captura (BETA): una URL de un solo uso que envías a tu
|
|
145
|
+
* cliente para que fotografíe su INE o pasaporte desde el celular. Los datos
|
|
146
|
+
* llegan a tu destino (`destinationId`), no a esta respuesta.
|
|
147
|
+
*
|
|
148
|
+
* Crear el enlace no gasta tokens; el escaneo del invitado gasta uno y se
|
|
149
|
+
* reembolsa si la extracción o la entrega fallan.
|
|
150
|
+
*
|
|
151
|
+
* @throws {IneExtractorError} `DESTINATION_NOT_FOUND`, `TOO_MANY_LIVE_LINKS`,
|
|
152
|
+
* `INVALID_REFERENCE`, `NOT_FOUND` (enlaces desactivados en tu cuenta)…
|
|
153
|
+
*/
|
|
154
|
+
async createCaptureLink(input) {
|
|
155
|
+
if (!input || !input.destinationId) {
|
|
156
|
+
throw new IneExtractorError("Falta `destinationId`.", "INVALID_INPUT");
|
|
157
|
+
}
|
|
158
|
+
const payload = {
|
|
159
|
+
destinationId: input.destinationId,
|
|
160
|
+
documentType: input.documentType ?? "ine"
|
|
161
|
+
};
|
|
162
|
+
if (input.requireBack) payload.requireBack = true;
|
|
163
|
+
if (input.reference !== void 0) payload.reference = input.reference;
|
|
164
|
+
if (input.name !== void 0) payload.name = input.name;
|
|
165
|
+
if (input.requesterName !== void 0) payload.requesterName = input.requesterName;
|
|
166
|
+
const body = await this.request("/capture-links", jsonRequest(payload));
|
|
167
|
+
return {
|
|
168
|
+
id: body.id,
|
|
169
|
+
url: body.url,
|
|
170
|
+
reference: body.reference ?? null,
|
|
171
|
+
documentType: body.documentType,
|
|
172
|
+
requireBack: body.requireBack,
|
|
173
|
+
askGuestPersona: body.askGuestPersona,
|
|
174
|
+
expiresAt: body.expiresAt
|
|
175
|
+
};
|
|
124
176
|
}
|
|
125
177
|
async request(path, init) {
|
|
126
178
|
const controller = new AbortController();
|
|
@@ -186,8 +238,66 @@ function jsonRequest(payload) {
|
|
|
186
238
|
body: JSON.stringify(payload)
|
|
187
239
|
};
|
|
188
240
|
}
|
|
241
|
+
|
|
242
|
+
// src/webhooks.ts
|
|
243
|
+
var SIGNATURE_HEADER = "X-Signature";
|
|
244
|
+
var SIGNATURE_TIMESTAMP_HEADER = "X-Signature-Timestamp";
|
|
245
|
+
var IDEMPOTENCY_KEY_HEADER = "X-Idempotency-Key";
|
|
246
|
+
var DEFAULT_TOLERANCE_S = 300;
|
|
247
|
+
async function verifyWebhookSignature(input) {
|
|
248
|
+
const { secret, signature, timestamp } = input;
|
|
249
|
+
if (!secret || !signature || !timestamp || !/^\d+$/.test(timestamp)) return false;
|
|
250
|
+
const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_S;
|
|
251
|
+
if (tolerance > 0) {
|
|
252
|
+
const now = input.now ?? Math.floor(Date.now() / 1e3);
|
|
253
|
+
if (Math.abs(now - Number(timestamp)) > tolerance) return false;
|
|
254
|
+
}
|
|
255
|
+
const body = typeof input.rawBody === "string" ? input.rawBody : new TextDecoder().decode(input.rawBody);
|
|
256
|
+
const expected = "sha256=" + await hmacSha256Hex(secret, `${timestamp}.${body}`);
|
|
257
|
+
return constantTimeEqual(expected, signature);
|
|
258
|
+
}
|
|
259
|
+
async function parseWebhook(input) {
|
|
260
|
+
if (!await verifyWebhookSignature(input)) {
|
|
261
|
+
throw new IneExtractorError("Firma de webhook inv\xE1lida o vencida.", "INVALID_SIGNATURE");
|
|
262
|
+
}
|
|
263
|
+
const body = typeof input.rawBody === "string" ? input.rawBody : new TextDecoder().decode(input.rawBody);
|
|
264
|
+
try {
|
|
265
|
+
return JSON.parse(body);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
throw new IneExtractorError("El cuerpo del webhook no es JSON.", "INVALID_INPUT", { cause: err });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function hmacSha256Hex(secret, message) {
|
|
271
|
+
const enc = new TextEncoder();
|
|
272
|
+
const subtle = await getSubtle();
|
|
273
|
+
const key = await subtle.importKey(
|
|
274
|
+
"raw",
|
|
275
|
+
enc.encode(secret),
|
|
276
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
277
|
+
false,
|
|
278
|
+
["sign"]
|
|
279
|
+
);
|
|
280
|
+
const sig = new Uint8Array(await subtle.sign("HMAC", key, enc.encode(message)));
|
|
281
|
+
return Array.from(sig, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
282
|
+
}
|
|
283
|
+
async function getSubtle() {
|
|
284
|
+
if (globalThis.crypto?.subtle) return globalThis.crypto.subtle;
|
|
285
|
+
const { webcrypto } = await import("crypto");
|
|
286
|
+
return webcrypto.subtle;
|
|
287
|
+
}
|
|
288
|
+
function constantTimeEqual(a, b) {
|
|
289
|
+
if (a.length !== b.length) return false;
|
|
290
|
+
let diff = 0;
|
|
291
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
292
|
+
return diff === 0;
|
|
293
|
+
}
|
|
189
294
|
export {
|
|
295
|
+
IDEMPOTENCY_KEY_HEADER,
|
|
190
296
|
IneExtractorClient,
|
|
191
|
-
IneExtractorError
|
|
297
|
+
IneExtractorError,
|
|
298
|
+
SIGNATURE_HEADER,
|
|
299
|
+
SIGNATURE_TIMESTAMP_HEADER,
|
|
300
|
+
parseWebhook,
|
|
301
|
+
verifyWebhookSignature
|
|
192
302
|
};
|
|
193
303
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/client.ts"],"sourcesContent":["/**\n * Códigos de error que devuelve la API de Extraer Datos de INE, más algunos\n * códigos propios del SDK (`NETWORK_ERROR`, `TIMEOUT`).\n *\n * @see https://extraerdatosdeine.com/docs\n */\nexport type IneErrorCode =\n // 401\n | 'MISSING_API_KEY'\n | 'INVALID_API_KEY'\n // 400\n | 'MISSING_IMAGE'\n | 'INVALID_IMAGE_FORMAT'\n | 'IMAGE_TOO_LARGE'\n | 'INVALID_BASE64'\n | 'INVALID_MULTIPART'\n | 'URL_FETCH_FAILED'\n | 'URL_INVALID'\n | 'URL_BLOCKED'\n | 'URL_TIMEOUT'\n // 415\n | 'UNSUPPORTED_CONTENT_TYPE'\n // 402\n | 'INSUFFICIENT_TOKENS'\n // 422\n | 'LOW_IMAGE_QUALITY'\n // 500\n | 'EXTRACTION_FAILED'\n | 'PROCESSING_ERROR'\n | 'INTERNAL_ERROR'\n // SDK-only\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'INVALID_INPUT'\n // forward-compatible: unknown server codes\n | (string & {})\n\n/**\n * Error lanzado por el SDK ante cualquier respuesta no exitosa de la API\n * o un fallo de red/timeout. Inspecciona `code` para reaccionar de forma\n * programática.\n *\n * @example\n * try {\n * await client.extract({ front })\n * } catch (err) {\n * if (err instanceof IneExtractorError && err.code === 'INSUFFICIENT_TOKENS') {\n * console.log('Recarga tokens en', err.enrollUrl)\n * }\n * }\n */\nexport class IneExtractorError extends Error {\n /** Código de error legible por máquina. */\n readonly code: IneErrorCode\n /** Status HTTP de la respuesta (ausente en errores de red). */\n readonly status?: number\n /** ID de la extracción, cuando aplica (LOW_IMAGE_QUALITY / EXTRACTION_FAILED). */\n readonly extractionId?: string\n /** Campos que no se pudieron leer (LOW_IMAGE_QUALITY). */\n readonly missingFields?: string[]\n /** URL para configurar auto-recarga (algunos INSUFFICIENT_TOKENS). */\n readonly enrollUrl?: string\n /** Motivo de bloqueo (algunos INSUFFICIENT_TOKENS). */\n readonly blockReason?: string\n /** Cuerpo crudo de la respuesta de error, por si necesitas más detalle. */\n readonly response?: unknown\n\n constructor(\n message: string,\n code: IneErrorCode,\n init: {\n status?: number\n extractionId?: string\n missingFields?: string[]\n enrollUrl?: string\n blockReason?: string\n response?: unknown\n cause?: unknown\n } = {}\n ) {\n super(message, init.cause !== undefined ? { cause: init.cause } : undefined)\n this.name = 'IneExtractorError'\n this.code = code\n this.status = init.status\n this.extractionId = init.extractionId\n this.missingFields = init.missingFields\n this.enrollUrl = init.enrollUrl\n this.blockReason = init.blockReason\n this.response = init.response\n // Restore prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, IneExtractorError.prototype)\n }\n\n /** Construye el error a partir de una respuesta de la API. */\n static fromResponse(status: number, body: unknown): IneExtractorError {\n const b = (body ?? {}) as Record<string, unknown>\n const code = (typeof b.code === 'string' ? b.code : 'INTERNAL_ERROR') as IneErrorCode\n const message =\n (typeof b.error === 'string' && b.error) ||\n `La API respondió con status ${status}`\n return new IneExtractorError(message, code, {\n status,\n extractionId: typeof b.extraction_id === 'string' ? b.extraction_id : undefined,\n missingFields: Array.isArray(b.missing_fields)\n ? (b.missing_fields as string[])\n : undefined,\n enrollUrl: typeof b.enroll_url === 'string' ? b.enroll_url : undefined,\n blockReason: typeof b.block_reason === 'string' ? b.block_reason : undefined,\n response: body,\n })\n }\n}\n","import { IneExtractorError } from './errors.js'\nimport type {\n BinaryImage,\n ExtractInput,\n ExtractResult,\n ImageSource,\n IneExtractorOptions,\n} from './types.js'\n\nconst DEFAULT_BASE_URL = 'https://extraerdatosdeine.com/api/v1'\nconst DEFAULT_TIMEOUT_MS = 60_000\n\n/**\n * Cliente para la API de Extraer Datos de INE.\n *\n * @example\n * import { readFile } from 'node:fs/promises'\n * import { IneExtractorClient } from 'extraer-datos-ine'\n *\n * const client = new IneExtractorClient({ apiKey: process.env.INE_API_KEY! })\n * const front = await readFile('./ine_frente.jpg')\n * const back = await readFile('./ine_reverso.jpg')\n * const { data } = await client.extract({ front, back })\n * console.log(data.curp, data.claveElector)\n *\n * @see https://extraerdatosdeine.com/docs\n */\nexport class IneExtractorClient {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly timeoutMs: number\n private readonly fetchImpl: typeof fetch\n\n constructor(options: IneExtractorOptions) {\n if (!options || !options.apiKey) {\n throw new IneExtractorError('Falta `apiKey` en las opciones del cliente.', 'INVALID_INPUT')\n }\n const fetchImpl = options.fetch ?? globalThis.fetch\n if (typeof fetchImpl !== 'function') {\n throw new IneExtractorError(\n 'No hay `fetch` disponible. Usa Node 18+ o pasa `fetch` en las opciones.',\n 'INVALID_INPUT'\n )\n }\n this.apiKey = options.apiKey\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n this.fetchImpl = fetchImpl\n }\n\n /**\n * Consulta el saldo de tokens de tu cuenta.\n * @returns número de tokens disponibles.\n */\n async getBalance(): Promise<number> {\n const body = await this.request('/balance', { method: 'GET' })\n return Number((body as { balance: number }).balance)\n }\n\n /**\n * Extrae los datos de una credencial INE/IFE.\n *\n * El método de envío se elige automáticamente según el tipo de `front`:\n * binario → multipart; `{ base64 }` o string → JSON base64; `{ url }` → JSON URL.\n * `front` y `back` deben ser del mismo tipo (la API no mezcla métodos).\n *\n * @throws {IneExtractorError} ante saldo insuficiente, imagen ilegible,\n * formato inválido, error de red, timeout, etc. Revisa `error.code`.\n */\n async extract(input: ExtractInput): Promise<ExtractResult> {\n if (!input || input.front === undefined || input.front === null) {\n throw new IneExtractorError('Falta la imagen frontal (`front`).', 'MISSING_IMAGE')\n }\n\n const frontKind = kindOf(input.front)\n if (input.back !== undefined && kindOf(input.back) !== frontKind) {\n throw new IneExtractorError(\n '`front` y `back` deben ser del mismo tipo (ambos binarios, base64 o url).',\n 'INVALID_INPUT'\n )\n }\n\n let init: RequestInit\n if (frontKind === 'url') {\n const payload: Record<string, string> = {\n image_front_url: (input.front as { url: string }).url,\n }\n if (input.back) payload.image_back_url = (input.back as { url: string }).url\n init = jsonRequest(payload)\n } else if (frontKind === 'base64') {\n const payload: Record<string, string> = { image_front: asBase64(input.front) }\n if (input.back) payload.image_back = asBase64(input.back)\n init = jsonRequest(payload)\n } else {\n const form = new FormData()\n form.append('image_front', toBlob(input.front as BinaryImage, input.frontMimeType))\n if (input.back) {\n form.append('image_back', toBlob(input.back as BinaryImage, input.backMimeType))\n }\n init = { method: 'POST', body: form }\n }\n\n const body = (await this.request('/extract', init)) as {\n extraction_id: string\n data: ExtractResult['data']\n tokens_remaining: number\n upload_method: string\n }\n\n return {\n extractionId: body.extraction_id,\n data: body.data,\n tokensRemaining: body.tokens_remaining,\n uploadMethod: body.upload_method,\n }\n }\n\n private async request(path: string, init: RequestInit): Promise<unknown> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\n\n let res: Response\n try {\n res = await this.fetchImpl(this.baseUrl + path, {\n ...init,\n signal: controller.signal,\n headers: { 'X-API-Key': this.apiKey, ...(init.headers ?? {}) },\n })\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new IneExtractorError(\n `La petición excedió el timeout de ${this.timeoutMs} ms.`,\n 'TIMEOUT',\n { cause: err }\n )\n }\n throw new IneExtractorError(\n err instanceof Error ? err.message : 'Error de red',\n 'NETWORK_ERROR',\n { cause: err }\n )\n } finally {\n clearTimeout(timer)\n }\n\n const body = await res.json().catch(() => ({}))\n if (!res.ok || (body as { success?: boolean })?.success === false) {\n throw IneExtractorError.fromResponse(res.status, body)\n }\n return body\n }\n}\n\ntype SourceKind = 'binary' | 'base64' | 'url'\n\nfunction kindOf(src: ImageSource): SourceKind {\n if (typeof src === 'string') return 'base64'\n if (src instanceof Uint8Array || src instanceof ArrayBuffer) return 'binary'\n if (src instanceof Blob) return 'binary'\n if (typeof src === 'object' && src !== null) {\n if ('url' in src) return 'url'\n if ('base64' in src) return 'base64'\n }\n throw new IneExtractorError(\n 'Fuente de imagen no soportada. Usa Uint8Array, ArrayBuffer, Blob, { base64 } o { url }.',\n 'INVALID_INPUT'\n )\n}\n\nfunction asBase64(src: ImageSource): string {\n if (typeof src === 'string') return src\n if (typeof src === 'object' && src !== null && 'base64' in src) return src.base64\n throw new IneExtractorError('Se esperaba una cadena base64.', 'INVALID_INPUT')\n}\n\nfunction toBlob(src: BinaryImage, mimeType = 'image/jpeg'): Blob {\n if (src instanceof Blob) return src\n if (src instanceof ArrayBuffer) return new Blob([src], { type: mimeType })\n // Uint8Array: copy exactly its bytes into a standalone ArrayBuffer so the\n // result is a valid BlobPart regardless of the backing buffer kind.\n const bytes = src.buffer.slice(src.byteOffset, src.byteOffset + src.byteLength)\n return new Blob([bytes as ArrayBuffer], { type: mimeType })\n}\n\nfunction jsonRequest(payload: Record<string, string>): RequestInit {\n return {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/webhooks.ts"],"sourcesContent":["/**\n * Códigos de error que devuelve la API de Extraer Datos de INE, más algunos\n * códigos propios del SDK (`NETWORK_ERROR`, `TIMEOUT`).\n *\n * @see https://extraerdatosdeine.com/docs\n */\nexport type IneErrorCode =\n // 401\n | 'MISSING_API_KEY'\n | 'INVALID_API_KEY'\n // 400\n | 'MISSING_IMAGE'\n | 'INVALID_IMAGE_FORMAT'\n | 'IMAGE_TOO_LARGE'\n | 'INVALID_BASE64'\n | 'INVALID_MULTIPART'\n | 'URL_FETCH_FAILED'\n | 'URL_INVALID'\n | 'URL_BLOCKED'\n | 'URL_TIMEOUT'\n | 'DESTINATION_NOT_FOUND'\n | 'INVALID_NAME'\n | 'INVALID_REFERENCE'\n | 'INVALID_REQUESTER_NAME'\n | 'INVALID_DOCUMENT_TYPE'\n | 'TOO_MANY_LIVE_LINKS'\n // 404 (enlaces de captura desactivados en la cuenta)\n | 'NOT_FOUND'\n // 415\n | 'UNSUPPORTED_CONTENT_TYPE'\n // 402\n | 'INSUFFICIENT_TOKENS'\n // 422\n | 'LOW_IMAGE_QUALITY'\n // 500\n | 'EXTRACTION_FAILED'\n | 'PROCESSING_ERROR'\n | 'INTERNAL_ERROR'\n // SDK-only\n | 'NETWORK_ERROR'\n | 'TIMEOUT'\n | 'INVALID_INPUT'\n | 'INVALID_SIGNATURE'\n // forward-compatible: unknown server codes\n | (string & {})\n\n/**\n * Error lanzado por el SDK ante cualquier respuesta no exitosa de la API\n * o un fallo de red/timeout. Inspecciona `code` para reaccionar de forma\n * programática.\n *\n * @example\n * try {\n * await client.extract({ front })\n * } catch (err) {\n * if (err instanceof IneExtractorError && err.code === 'INSUFFICIENT_TOKENS') {\n * console.log('Recarga tokens en', err.enrollUrl)\n * }\n * }\n */\nexport class IneExtractorError extends Error {\n /** Código de error legible por máquina. */\n readonly code: IneErrorCode\n /** Status HTTP de la respuesta (ausente en errores de red). */\n readonly status?: number\n /** ID de la extracción, cuando aplica (LOW_IMAGE_QUALITY / EXTRACTION_FAILED). */\n readonly extractionId?: string\n /** Campos que no se pudieron leer (LOW_IMAGE_QUALITY). */\n readonly missingFields?: string[]\n /** URL para configurar auto-recarga (algunos INSUFFICIENT_TOKENS). */\n readonly enrollUrl?: string\n /** Motivo de bloqueo (algunos INSUFFICIENT_TOKENS). */\n readonly blockReason?: string\n /** Cuerpo crudo de la respuesta de error, por si necesitas más detalle. */\n readonly response?: unknown\n\n constructor(\n message: string,\n code: IneErrorCode,\n init: {\n status?: number\n extractionId?: string\n missingFields?: string[]\n enrollUrl?: string\n blockReason?: string\n response?: unknown\n cause?: unknown\n } = {}\n ) {\n super(message, init.cause !== undefined ? { cause: init.cause } : undefined)\n this.name = 'IneExtractorError'\n this.code = code\n this.status = init.status\n this.extractionId = init.extractionId\n this.missingFields = init.missingFields\n this.enrollUrl = init.enrollUrl\n this.blockReason = init.blockReason\n this.response = init.response\n // Restore prototype chain for instanceof across transpile targets.\n Object.setPrototypeOf(this, IneExtractorError.prototype)\n }\n\n /** Construye el error a partir de una respuesta de la API. */\n static fromResponse(status: number, body: unknown): IneExtractorError {\n const b = (body ?? {}) as Record<string, unknown>\n const code = (typeof b.code === 'string' ? b.code : 'INTERNAL_ERROR') as IneErrorCode\n const message =\n (typeof b.error === 'string' && b.error) ||\n `La API respondió con status ${status}`\n return new IneExtractorError(message, code, {\n status,\n extractionId: typeof b.extraction_id === 'string' ? b.extraction_id : undefined,\n missingFields: Array.isArray(b.missing_fields)\n ? (b.missing_fields as string[])\n : undefined,\n enrollUrl: typeof b.enroll_url === 'string' ? b.enroll_url : undefined,\n blockReason: typeof b.block_reason === 'string' ? b.block_reason : undefined,\n response: body,\n })\n }\n}\n","import { IneExtractorError } from './errors.js'\nimport type {\n BinaryImage,\n CaptureLink,\n CreateCaptureLinkInput,\n DeliveryReport,\n ExtractInput,\n ExtractResult,\n ImageSource,\n IneExtractorOptions,\n} from './types.js'\n\nconst DEFAULT_BASE_URL = 'https://extraerdatosdeine.com/api/v1'\nconst DEFAULT_TIMEOUT_MS = 60_000\n\n/**\n * Cliente para la API de Extraer Datos de INE.\n *\n * @example\n * import { readFile } from 'node:fs/promises'\n * import { IneExtractorClient } from 'extraer-datos-ine'\n *\n * const client = new IneExtractorClient({ apiKey: process.env.INE_API_KEY! })\n * const front = await readFile('./ine_frente.jpg')\n * const back = await readFile('./ine_reverso.jpg')\n * const { data } = await client.extract({ front, back })\n * console.log(data.curp, data.claveElector)\n *\n * @see https://extraerdatosdeine.com/docs\n */\nexport class IneExtractorClient {\n private readonly apiKey: string\n private readonly baseUrl: string\n private readonly timeoutMs: number\n private readonly fetchImpl: typeof fetch\n\n constructor(options: IneExtractorOptions) {\n if (!options || !options.apiKey) {\n throw new IneExtractorError('Falta `apiKey` en las opciones del cliente.', 'INVALID_INPUT')\n }\n const fetchImpl = options.fetch ?? globalThis.fetch\n if (typeof fetchImpl !== 'function') {\n throw new IneExtractorError(\n 'No hay `fetch` disponible. Usa Node 18+ o pasa `fetch` en las opciones.',\n 'INVALID_INPUT'\n )\n }\n this.apiKey = options.apiKey\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/$/, '')\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS\n this.fetchImpl = fetchImpl\n }\n\n /**\n * Consulta el saldo de tokens de tu cuenta.\n * @returns número de tokens disponibles.\n */\n async getBalance(): Promise<number> {\n const body = await this.request('/balance', { method: 'GET' })\n return Number((body as { balance: number }).balance)\n }\n\n /**\n * Extrae los datos de una credencial INE/IFE.\n *\n * El método de envío se elige automáticamente según el tipo de `front`:\n * binario → multipart; `{ base64 }` o string → JSON base64; `{ url }` → JSON URL.\n * `front` y `back` deben ser del mismo tipo (la API no mezcla métodos).\n *\n * @throws {IneExtractorError} ante saldo insuficiente, imagen ilegible,\n * formato inválido, error de red, timeout, etc. Revisa `error.code`.\n */\n async extract(input: ExtractInput): Promise<ExtractResult> {\n if (!input || input.front === undefined || input.front === null) {\n throw new IneExtractorError('Falta la imagen frontal (`front`).', 'MISSING_IMAGE')\n }\n\n const frontKind = kindOf(input.front)\n if (input.back !== undefined && kindOf(input.back) !== frontKind) {\n throw new IneExtractorError(\n '`front` y `back` deben ser del mismo tipo (ambos binarios, base64 o url).',\n 'INVALID_INPUT'\n )\n }\n\n let init: RequestInit\n if (frontKind === 'url') {\n const payload: Record<string, string> = {\n image_front_url: (input.front as { url: string }).url,\n }\n if (input.back) payload.image_back_url = (input.back as { url: string }).url\n init = jsonRequest(payload)\n } else if (frontKind === 'base64') {\n const payload: Record<string, string> = { image_front: asBase64(input.front) }\n if (input.back) payload.image_back = asBase64(input.back)\n init = jsonRequest(payload)\n } else {\n const form = new FormData()\n form.append('image_front', toBlob(input.front as BinaryImage, input.frontMimeType))\n if (input.back) {\n form.append('image_back', toBlob(input.back as BinaryImage, input.backMimeType))\n }\n init = { method: 'POST', body: form }\n }\n\n if (input.destinationId) {\n init.headers = {\n ...(init.headers as Record<string, string> | undefined),\n 'X-Destination-Id': input.destinationId,\n }\n }\n\n const body = (await this.request('/extract', init)) as {\n extraction_id: string\n data: ExtractResult['data']\n tokens_remaining: number\n upload_method: string\n delivery?: {\n destination_id: string\n succeeded: boolean\n http_status: number | null\n latency_ms: number | null\n error_code: string | null\n }\n }\n\n const result: ExtractResult = {\n extractionId: body.extraction_id,\n data: body.data,\n tokensRemaining: body.tokens_remaining,\n uploadMethod: body.upload_method,\n }\n if (body.delivery) {\n const d = body.delivery\n const delivery: DeliveryReport = {\n destinationId: d.destination_id,\n succeeded: d.succeeded,\n httpStatus: d.http_status ?? null,\n latencyMs: d.latency_ms ?? null,\n errorCode: d.error_code ?? null,\n }\n result.delivery = delivery\n }\n return result\n }\n\n /**\n * Crea un enlace de captura (BETA): una URL de un solo uso que envías a tu\n * cliente para que fotografíe su INE o pasaporte desde el celular. Los datos\n * llegan a tu destino (`destinationId`), no a esta respuesta.\n *\n * Crear el enlace no gasta tokens; el escaneo del invitado gasta uno y se\n * reembolsa si la extracción o la entrega fallan.\n *\n * @throws {IneExtractorError} `DESTINATION_NOT_FOUND`, `TOO_MANY_LIVE_LINKS`,\n * `INVALID_REFERENCE`, `NOT_FOUND` (enlaces desactivados en tu cuenta)…\n */\n async createCaptureLink(input: CreateCaptureLinkInput): Promise<CaptureLink> {\n if (!input || !input.destinationId) {\n throw new IneExtractorError('Falta `destinationId`.', 'INVALID_INPUT')\n }\n const payload: Record<string, unknown> = {\n destinationId: input.destinationId,\n documentType: input.documentType ?? 'ine',\n }\n if (input.requireBack) payload.requireBack = true\n if (input.reference !== undefined) payload.reference = input.reference\n if (input.name !== undefined) payload.name = input.name\n if (input.requesterName !== undefined) payload.requesterName = input.requesterName\n\n const body = (await this.request('/capture-links', jsonRequest(payload))) as CaptureLink\n return {\n id: body.id,\n url: body.url,\n reference: body.reference ?? null,\n documentType: body.documentType,\n requireBack: body.requireBack,\n askGuestPersona: body.askGuestPersona,\n expiresAt: body.expiresAt,\n }\n }\n\n private async request(path: string, init: RequestInit): Promise<unknown> {\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\n\n let res: Response\n try {\n res = await this.fetchImpl(this.baseUrl + path, {\n ...init,\n signal: controller.signal,\n headers: { 'X-API-Key': this.apiKey, ...(init.headers ?? {}) },\n })\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new IneExtractorError(\n `La petición excedió el timeout de ${this.timeoutMs} ms.`,\n 'TIMEOUT',\n { cause: err }\n )\n }\n throw new IneExtractorError(\n err instanceof Error ? err.message : 'Error de red',\n 'NETWORK_ERROR',\n { cause: err }\n )\n } finally {\n clearTimeout(timer)\n }\n\n const body = await res.json().catch(() => ({}))\n if (!res.ok || (body as { success?: boolean })?.success === false) {\n throw IneExtractorError.fromResponse(res.status, body)\n }\n return body\n }\n}\n\ntype SourceKind = 'binary' | 'base64' | 'url'\n\nfunction kindOf(src: ImageSource): SourceKind {\n if (typeof src === 'string') return 'base64'\n if (src instanceof Uint8Array || src instanceof ArrayBuffer) return 'binary'\n if (src instanceof Blob) return 'binary'\n if (typeof src === 'object' && src !== null) {\n if ('url' in src) return 'url'\n if ('base64' in src) return 'base64'\n }\n throw new IneExtractorError(\n 'Fuente de imagen no soportada. Usa Uint8Array, ArrayBuffer, Blob, { base64 } o { url }.',\n 'INVALID_INPUT'\n )\n}\n\nfunction asBase64(src: ImageSource): string {\n if (typeof src === 'string') return src\n if (typeof src === 'object' && src !== null && 'base64' in src) return src.base64\n throw new IneExtractorError('Se esperaba una cadena base64.', 'INVALID_INPUT')\n}\n\nfunction toBlob(src: BinaryImage, mimeType = 'image/jpeg'): Blob {\n if (src instanceof Blob) return src\n if (src instanceof ArrayBuffer) return new Blob([src], { type: mimeType })\n // Uint8Array: copy exactly its bytes into a standalone ArrayBuffer so the\n // result is a valid BlobPart regardless of the backing buffer kind.\n const bytes = src.buffer.slice(src.byteOffset, src.byteOffset + src.byteLength)\n return new Blob([bytes as ArrayBuffer], { type: mimeType })\n}\n\nfunction jsonRequest(payload: Record<string, unknown>): RequestInit {\n return {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }\n}\n","import { IneExtractorError } from './errors.js'\nimport type { WebhookEvent } from './types.js'\n\n/** Header con `sha256=<hex>`. */\nexport const SIGNATURE_HEADER = 'X-Signature'\n/** Header con los segundos Unix que cubre la firma. */\nexport const SIGNATURE_TIMESTAMP_HEADER = 'X-Signature-Timestamp'\n/** Header con la clave de idempotencia de la entrega. */\nexport const IDEMPOTENCY_KEY_HEADER = 'X-Idempotency-Key'\n\nconst DEFAULT_TOLERANCE_S = 300\n\nexport interface VerifyWebhookInput {\n /** El secreto del destino (`whsec_…`), mostrado una vez al crearlo. */\n secret: string\n /** El cuerpo CRUDO, exactamente como llegó. No lo re-serialices. */\n rawBody: string | Uint8Array\n /** Valor del header `X-Signature`. */\n signature: string | null | undefined\n /** Valor del header `X-Signature-Timestamp`. */\n timestamp: string | null | undefined\n /**\n * Antigüedad máxima aceptada, en segundos (por defecto 300). Protege contra\n * la repetición de una entrega capturada. `0` desactiva la comprobación.\n */\n toleranceSeconds?: number\n /** Solo para pruebas: segundos Unix \"actuales\". */\n now?: number\n}\n\n/**\n * Comprueba que una entrega a tu webhook viene de Extraer Datos de INE.\n *\n * La firma es `sha256=` + HMAC-SHA256(secreto, `timestamp + \".\" + cuerpo`).\n * Devuelve false (nunca lanza) ante una firma ausente, malformada, incorrecta\n * o fuera de tiempo.\n */\nexport async function verifyWebhookSignature(input: VerifyWebhookInput): Promise<boolean> {\n const { secret, signature, timestamp } = input\n if (!secret || !signature || !timestamp || !/^\\d+$/.test(timestamp)) return false\n\n const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_S\n if (tolerance > 0) {\n const now = input.now ?? Math.floor(Date.now() / 1000)\n if (Math.abs(now - Number(timestamp)) > tolerance) return false\n }\n\n const body =\n typeof input.rawBody === 'string' ? input.rawBody : new TextDecoder().decode(input.rawBody)\n const expected = 'sha256=' + (await hmacSha256Hex(secret, `${timestamp}.${body}`))\n return constantTimeEqual(expected, signature)\n}\n\n/**\n * Verifica la firma y devuelve el cuerpo ya interpretado.\n *\n * @throws {IneExtractorError} `INVALID_SIGNATURE` si la firma no es válida.\n *\n * @example\n * // Express: usa express.raw({ type: 'application/json' }) en esta ruta.\n * const event = await parseWebhook({\n * secret: process.env.INE_WEBHOOK_SECRET!,\n * rawBody: req.body,\n * signature: req.get('X-Signature'),\n * timestamp: req.get('X-Signature-Timestamp'),\n * })\n * if (event.event === 'extraction.completed') console.log(event.reference, event.data.curp)\n */\nexport async function parseWebhook(input: VerifyWebhookInput): Promise<WebhookEvent> {\n if (!(await verifyWebhookSignature(input))) {\n throw new IneExtractorError('Firma de webhook inválida o vencida.', 'INVALID_SIGNATURE')\n }\n const body =\n typeof input.rawBody === 'string' ? input.rawBody : new TextDecoder().decode(input.rawBody)\n try {\n return JSON.parse(body) as WebhookEvent\n } catch (err) {\n throw new IneExtractorError('El cuerpo del webhook no es JSON.', 'INVALID_INPUT', { cause: err })\n }\n}\n\nasync function hmacSha256Hex(secret: string, message: string): Promise<string> {\n const enc = new TextEncoder()\n const subtle = await getSubtle()\n const key = await subtle.importKey(\n 'raw',\n enc.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n )\n const sig = new Uint8Array(await subtle.sign('HMAC', key, enc.encode(message)))\n return Array.from(sig, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\n/** Web Crypto: global en Node 19+, Deno, Bun, Workers; en Node 18 viene de node:crypto. */\nasync function getSubtle(): Promise<SubtleCrypto> {\n if (globalThis.crypto?.subtle) return globalThis.crypto.subtle\n const { webcrypto } = await import('node:crypto')\n return webcrypto.subtle as SubtleCrypto\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED