soulprint-verify 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/document/cedula-validator.d.ts +34 -2
- package/dist/document/cedula-validator.js +102 -13
- package/dist/document/countries/AR.d.ts +25 -0
- package/dist/document/countries/AR.js +85 -0
- package/dist/document/countries/BR.d.ts +22 -0
- package/dist/document/countries/BR.js +89 -0
- package/dist/document/countries/CL.d.ts +20 -0
- package/dist/document/countries/CL.js +82 -0
- package/dist/document/countries/CO.d.ts +16 -0
- package/dist/document/countries/CO.js +77 -0
- package/dist/document/countries/MX.d.ts +22 -0
- package/dist/document/countries/MX.js +77 -0
- package/dist/document/countries/PE.d.ts +19 -0
- package/dist/document/countries/PE.js +64 -0
- package/dist/document/countries/VE.d.ts +19 -0
- package/dist/document/countries/VE.js +66 -0
- package/dist/document/registry.d.ts +38 -0
- package/dist/document/registry.js +97 -0
- package/dist/document/verifier.interface.d.ts +85 -0
- package/dist/document/verifier.interface.js +16 -0
- package/dist/face/face_match.py +249 -56
- package/dist/index.d.ts +3 -1
- package/dist/index.js +10 -1
- package/package.json +3 -3
- package/src/face/face_match.py +249 -56
|
@@ -21,19 +21,51 @@ export declare function validateCedulaNumber(cedula: string): {
|
|
|
21
21
|
error?: string;
|
|
22
22
|
};
|
|
23
23
|
export declare function parseCedulaOCR(ocrText: string): DocumentValidationResult;
|
|
24
|
+
/**
|
|
25
|
+
* Calcula el dígito de control ICAO 9303 (estándar internacional MRTD).
|
|
26
|
+
* Algoritmo: suma ponderada con pesos 7, 3, 1 (cíclicos) sobre cada carácter.
|
|
27
|
+
*
|
|
28
|
+
* Tabla de valores:
|
|
29
|
+
* '0'-'9' → 0-9
|
|
30
|
+
* 'A'-'Z' → 10-35
|
|
31
|
+
* '<' → 0
|
|
32
|
+
*
|
|
33
|
+
* Resultado: suma mod 10
|
|
34
|
+
*
|
|
35
|
+
* Referencia: ICAO Doc 9303 Part 3, §4.9
|
|
36
|
+
*/
|
|
37
|
+
export declare function icaoCheckDigit(field: string): number;
|
|
38
|
+
/**
|
|
39
|
+
* Verifica si el dígito de control ICAO de un campo es correcto.
|
|
40
|
+
* `fieldWithCheck`: el campo + el dígito de control como último carácter.
|
|
41
|
+
*/
|
|
42
|
+
export declare function verifyCheckDigit(field: string, expected: string | number): {
|
|
43
|
+
valid: boolean;
|
|
44
|
+
computed: number;
|
|
45
|
+
expected: number;
|
|
46
|
+
};
|
|
24
47
|
/**
|
|
25
48
|
* Parsea el MRZ TD1 del reverso de la cédula colombiana digital.
|
|
26
49
|
* Formato: 3 líneas de 30 caracteres cada una.
|
|
27
50
|
*
|
|
51
|
+
* Línea 1 (TD1): IDCOL<NUMDOC<CHECK<<<<<<<<<<<<<<<<
|
|
52
|
+
* - [0-1] = tipo doc "ID"
|
|
53
|
+
* - [2-4] = país emisor "COL"
|
|
54
|
+
* - [5-14] = número documento (cédula, 9 chars relleno <)
|
|
55
|
+
* - [14] = check digit del número de documento
|
|
56
|
+
*
|
|
28
57
|
* Línea 2: DDMMYYCSEXEXPIRYNATCHECKNUMDOC<CHECK
|
|
29
58
|
* - [0-5] = fecha nacimiento YYMMDD
|
|
30
|
-
* - [6] =
|
|
59
|
+
* - [6] = check digit fecha nac
|
|
31
60
|
* - [7] = sexo M/F
|
|
32
61
|
* - [8-13] = fecha expiración YYMMDD
|
|
33
|
-
* - [14] =
|
|
62
|
+
* - [14] = check digit expiración
|
|
34
63
|
* - [15-17] = código país (COL)
|
|
35
64
|
* - [18-28] = número documento (cédula)
|
|
65
|
+
* - [29] = check digit compuesto
|
|
36
66
|
*
|
|
37
67
|
* Línea 3: APELLIDOS<<NOMBRES<<<...
|
|
68
|
+
*
|
|
69
|
+
* Todos los check digits se verifican con ICAO 9303 (peso 7/3/1 mod 10).
|
|
38
70
|
*/
|
|
39
71
|
export declare function parseMRZ(mrzText: string): DocumentValidationResult;
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.validateCedulaNumber = validateCedulaNumber;
|
|
13
13
|
exports.parseCedulaOCR = parseCedulaOCR;
|
|
14
|
+
exports.icaoCheckDigit = icaoCheckDigit;
|
|
15
|
+
exports.verifyCheckDigit = verifyCheckDigit;
|
|
14
16
|
exports.parseMRZ = parseMRZ;
|
|
15
17
|
// ── Rangos válidos de cédulas colombianas ─────────────────────────────────────
|
|
16
18
|
// Fuente: Registraduría Nacional — rangos históricos por décadas
|
|
@@ -127,24 +129,72 @@ function parseCedulaOCR(ocrText) {
|
|
|
127
129
|
raw_ocr: ocrText,
|
|
128
130
|
};
|
|
129
131
|
}
|
|
132
|
+
// ── ICAO 9303 check digit ──────────────────────────────────────────────────────
|
|
133
|
+
/**
|
|
134
|
+
* Calcula el dígito de control ICAO 9303 (estándar internacional MRTD).
|
|
135
|
+
* Algoritmo: suma ponderada con pesos 7, 3, 1 (cíclicos) sobre cada carácter.
|
|
136
|
+
*
|
|
137
|
+
* Tabla de valores:
|
|
138
|
+
* '0'-'9' → 0-9
|
|
139
|
+
* 'A'-'Z' → 10-35
|
|
140
|
+
* '<' → 0
|
|
141
|
+
*
|
|
142
|
+
* Resultado: suma mod 10
|
|
143
|
+
*
|
|
144
|
+
* Referencia: ICAO Doc 9303 Part 3, §4.9
|
|
145
|
+
*/
|
|
146
|
+
function icaoCheckDigit(field) {
|
|
147
|
+
const WEIGHTS = [7, 3, 1];
|
|
148
|
+
const charValue = (ch) => {
|
|
149
|
+
if (ch >= "0" && ch <= "9")
|
|
150
|
+
return parseInt(ch, 10);
|
|
151
|
+
if (ch >= "A" && ch <= "Z")
|
|
152
|
+
return ch.charCodeAt(0) - 65 + 10; // A=10, B=11...
|
|
153
|
+
return 0; // '<' y cualquier relleno = 0
|
|
154
|
+
};
|
|
155
|
+
let sum = 0;
|
|
156
|
+
for (let i = 0; i < field.length; i++) {
|
|
157
|
+
sum += charValue(field[i]) * WEIGHTS[i % 3];
|
|
158
|
+
}
|
|
159
|
+
return sum % 10;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Verifica si el dígito de control ICAO de un campo es correcto.
|
|
163
|
+
* `fieldWithCheck`: el campo + el dígito de control como último carácter.
|
|
164
|
+
*/
|
|
165
|
+
function verifyCheckDigit(field, expected) {
|
|
166
|
+
const computed = icaoCheckDigit(field);
|
|
167
|
+
const exp = typeof expected === "string" ? parseInt(expected, 10) : expected;
|
|
168
|
+
return { valid: computed === exp, computed, expected: exp };
|
|
169
|
+
}
|
|
130
170
|
// ── MRZ TD1 parser (reverso cédula digital colombiana) ────────────────────────
|
|
131
171
|
/**
|
|
132
172
|
* Parsea el MRZ TD1 del reverso de la cédula colombiana digital.
|
|
133
173
|
* Formato: 3 líneas de 30 caracteres cada una.
|
|
134
174
|
*
|
|
175
|
+
* Línea 1 (TD1): IDCOL<NUMDOC<CHECK<<<<<<<<<<<<<<<<
|
|
176
|
+
* - [0-1] = tipo doc "ID"
|
|
177
|
+
* - [2-4] = país emisor "COL"
|
|
178
|
+
* - [5-14] = número documento (cédula, 9 chars relleno <)
|
|
179
|
+
* - [14] = check digit del número de documento
|
|
180
|
+
*
|
|
135
181
|
* Línea 2: DDMMYYCSEXEXPIRYNATCHECKNUMDOC<CHECK
|
|
136
182
|
* - [0-5] = fecha nacimiento YYMMDD
|
|
137
|
-
* - [6] =
|
|
183
|
+
* - [6] = check digit fecha nac
|
|
138
184
|
* - [7] = sexo M/F
|
|
139
185
|
* - [8-13] = fecha expiración YYMMDD
|
|
140
|
-
* - [14] =
|
|
186
|
+
* - [14] = check digit expiración
|
|
141
187
|
* - [15-17] = código país (COL)
|
|
142
188
|
* - [18-28] = número documento (cédula)
|
|
189
|
+
* - [29] = check digit compuesto
|
|
143
190
|
*
|
|
144
191
|
* Línea 3: APELLIDOS<<NOMBRES<<<...
|
|
192
|
+
*
|
|
193
|
+
* Todos los check digits se verifican con ICAO 9303 (peso 7/3/1 mod 10).
|
|
145
194
|
*/
|
|
146
195
|
function parseMRZ(mrzText) {
|
|
147
196
|
const errors = [];
|
|
197
|
+
const warnings = [];
|
|
148
198
|
// Limpiar y encontrar líneas MRZ
|
|
149
199
|
const allLines = mrzText
|
|
150
200
|
.split("\n")
|
|
@@ -154,7 +204,22 @@ function parseMRZ(mrzText) {
|
|
|
154
204
|
if (lines.length < 2) {
|
|
155
205
|
return { valid: false, errors: ["MRZ incompleto — se necesitan al menos 2 líneas"] };
|
|
156
206
|
}
|
|
157
|
-
// Línea
|
|
207
|
+
// ── Línea 1: número de documento + check digit ───────────────────────────
|
|
208
|
+
const line1 = lines[0];
|
|
209
|
+
let docNumFromLine1;
|
|
210
|
+
if (line1.length >= 15) {
|
|
211
|
+
const docRaw = line1.slice(5, 14); // posiciones 5-13 (9 chars)
|
|
212
|
+
const checkCh = line1[14]; // posición 14 = check digit
|
|
213
|
+
const checkResult = verifyCheckDigit(docRaw, checkCh);
|
|
214
|
+
if (!checkResult.valid) {
|
|
215
|
+
errors.push(`MRZ línea 1: check digit inválido en número de documento ` +
|
|
216
|
+
`(calculado=${checkResult.computed}, encontrado=${checkResult.expected})`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
docNumFromLine1 = docRaw.replace(/</g, "").replace(/^0+/, "");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ── Línea 2: fecha nacimiento + sexo + expiración + check digits ─────────
|
|
158
223
|
const line2 = lines.find(l => /^\d{6}[0-9<][MF<]/.test(l));
|
|
159
224
|
if (!line2) {
|
|
160
225
|
return { valid: false, errors: ["No se encontró línea MRZ con datos biográficos"] };
|
|
@@ -163,14 +228,39 @@ function parseMRZ(mrzText) {
|
|
|
163
228
|
const mm = line2.slice(2, 4);
|
|
164
229
|
const dd = line2.slice(4, 6);
|
|
165
230
|
const sex = line2[7];
|
|
166
|
-
//
|
|
231
|
+
// Verificar check digit de fecha de nacimiento (posición 6)
|
|
232
|
+
const dobField = line2.slice(0, 6);
|
|
233
|
+
const dobCheck = line2[6];
|
|
234
|
+
const dobVerify = verifyCheckDigit(dobField, dobCheck);
|
|
235
|
+
if (!dobVerify.valid && dobCheck !== "<") {
|
|
236
|
+
errors.push(`MRZ: check digit inválido en fecha de nacimiento ` +
|
|
237
|
+
`(calculado=${dobVerify.computed}, encontrado=${dobVerify.expected})`);
|
|
238
|
+
}
|
|
239
|
+
// Verificar check digit de fecha de expiración (posición 14)
|
|
240
|
+
const expField = line2.slice(8, 14);
|
|
241
|
+
const expCheck = line2[14];
|
|
242
|
+
const expVerify = verifyCheckDigit(expField, expCheck);
|
|
243
|
+
if (!expVerify.valid && expCheck !== "<") {
|
|
244
|
+
warnings.push(`MRZ: check digit de expiración no coincide ` +
|
|
245
|
+
`(calculado=${expVerify.computed}, encontrado=${expVerify.expected})`);
|
|
246
|
+
}
|
|
247
|
+
// Inferir siglo: YY > 24 → 19xx, YY <= 24 → 20xx
|
|
167
248
|
const century = parseInt(yy) > 24 ? "19" : "20";
|
|
168
249
|
const fecha_nacimiento = `${century}${yy}-${mm}-${dd}`;
|
|
169
|
-
// Número de cédula:
|
|
170
|
-
|
|
171
|
-
|
|
250
|
+
// ── Número de cédula: prioridad línea 1 → fallback línea 2 pos 18-27 ────
|
|
251
|
+
let docNum;
|
|
252
|
+
if (docNumFromLine1) {
|
|
253
|
+
docNum = docNumFromLine1;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const raw = line2.slice(18, 29).replace(/</g, "").trim();
|
|
257
|
+
docNum = raw.replace(/^0+/, "");
|
|
258
|
+
}
|
|
172
259
|
const numValidation = validateCedulaNumber(docNum);
|
|
173
|
-
|
|
260
|
+
if (!numValidation.valid) {
|
|
261
|
+
errors.push(`Número en MRZ inválido: ${numValidation.error}`);
|
|
262
|
+
}
|
|
263
|
+
// ── Línea 3: nombre ───────────────────────────────────────────────────────
|
|
174
264
|
const line3 = allLines.find(l => l.includes("<<") &&
|
|
175
265
|
!/^\d{6}/.test(l) &&
|
|
176
266
|
/^[A-Z]{3,}<</.test(l));
|
|
@@ -179,13 +269,12 @@ function parseMRZ(mrzText) {
|
|
|
179
269
|
const parts = line3.split("<<");
|
|
180
270
|
const apellido = parts[0]?.replace(/</g, " ").trim();
|
|
181
271
|
const nombres = parts.slice(1).join(" ").replace(/</g, " ").replace(/\s+/g, " ").trim();
|
|
182
|
-
nombre = nombres && apellido
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
errors.push(`Número en MRZ inválido: ${numValidation.error}`);
|
|
272
|
+
nombre = nombres && apellido
|
|
273
|
+
? `${nombres} ${apellido}`.trim()
|
|
274
|
+
: (apellido || nombres);
|
|
186
275
|
}
|
|
187
276
|
return {
|
|
188
|
-
valid: errors.length === 0,
|
|
277
|
+
valid: errors.length === 0 && numValidation.valid,
|
|
189
278
|
cedula_number: numValidation.valid ? docNum : undefined,
|
|
190
279
|
nombre,
|
|
191
280
|
fecha_nacimiento,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🇦🇷 Argentina — Documento Nacional de Identidad (DNI)
|
|
3
|
+
* =========================================================
|
|
4
|
+
* Supported documents:
|
|
5
|
+
* - DNI (Documento Nacional de Identidad) — TD1 format since 2009
|
|
6
|
+
* - Old DNI (libreta) — pre-2009, no MRZ
|
|
7
|
+
*
|
|
8
|
+
* Issuing authority: Registro Nacional de las Personas (RENAPER)
|
|
9
|
+
* Format: 7–8 digits (new DNI), may have leading zeros
|
|
10
|
+
* MRZ: TD1 (3 lines × 30 chars) on back of card DNI
|
|
11
|
+
*
|
|
12
|
+
* Key fields:
|
|
13
|
+
* - "DOCUMENTO NACIONAL DE IDENTIDAD"
|
|
14
|
+
* - CUIL/CUIT derived: 20/DNI/1 (men) or 27/DNI/1 (women)
|
|
15
|
+
*
|
|
16
|
+
* Resources:
|
|
17
|
+
* - RENAPER: https://www.argentina.gob.ar/interior/renaper
|
|
18
|
+
* - DNI spec: TD1 ICAO 9303
|
|
19
|
+
*
|
|
20
|
+
* 👋 CONTRIBUTOR: implement parse(), parseMRZ()
|
|
21
|
+
* Status: STUB — contributions welcome!
|
|
22
|
+
*/
|
|
23
|
+
import type { CountryVerifier } from "../verifier.interface.js";
|
|
24
|
+
declare const AR: CountryVerifier;
|
|
25
|
+
export default AR;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 🇦🇷 Argentina — Documento Nacional de Identidad (DNI)
|
|
4
|
+
* =========================================================
|
|
5
|
+
* Supported documents:
|
|
6
|
+
* - DNI (Documento Nacional de Identidad) — TD1 format since 2009
|
|
7
|
+
* - Old DNI (libreta) — pre-2009, no MRZ
|
|
8
|
+
*
|
|
9
|
+
* Issuing authority: Registro Nacional de las Personas (RENAPER)
|
|
10
|
+
* Format: 7–8 digits (new DNI), may have leading zeros
|
|
11
|
+
* MRZ: TD1 (3 lines × 30 chars) on back of card DNI
|
|
12
|
+
*
|
|
13
|
+
* Key fields:
|
|
14
|
+
* - "DOCUMENTO NACIONAL DE IDENTIDAD"
|
|
15
|
+
* - CUIL/CUIT derived: 20/DNI/1 (men) or 27/DNI/1 (women)
|
|
16
|
+
*
|
|
17
|
+
* Resources:
|
|
18
|
+
* - RENAPER: https://www.argentina.gob.ar/interior/renaper
|
|
19
|
+
* - DNI spec: TD1 ICAO 9303
|
|
20
|
+
*
|
|
21
|
+
* 👋 CONTRIBUTOR: implement parse(), parseMRZ()
|
|
22
|
+
* Status: STUB — contributions welcome!
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
const AR = {
|
|
26
|
+
countryCode: "AR",
|
|
27
|
+
countryName: "Argentina",
|
|
28
|
+
documentTypes: ["dni"],
|
|
29
|
+
parse(ocrText) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
const text = ocrText.toUpperCase().replace(/\s+/g, " ").trim();
|
|
32
|
+
// ── Extract DNI number ────────────────────────────────────────────────
|
|
33
|
+
let doc_number;
|
|
34
|
+
const dniPatterns = [
|
|
35
|
+
/DNI[:\s#Nº]+([0-9][0-9.\s]{6,9})/,
|
|
36
|
+
/NÚMERO[:\s]+([0-9][0-9.\s]{6,9})/,
|
|
37
|
+
/(?<![0-9])(\d{2}\.?\d{3}\.?\d{3})(?![0-9])/, // XX.XXX.XXX
|
|
38
|
+
];
|
|
39
|
+
for (const pat of dniPatterns) {
|
|
40
|
+
const m = text.match(pat);
|
|
41
|
+
if (m) {
|
|
42
|
+
const candidate = m[1].replace(/[.\s]/g, "");
|
|
43
|
+
const v = AR.validate(candidate);
|
|
44
|
+
if (v.valid) {
|
|
45
|
+
doc_number = v.normalized;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const isArgentina = /ARGENTINA|RENAPER|DOCUMENTO NACIONAL/i.test(text);
|
|
51
|
+
if (!isArgentina)
|
|
52
|
+
errors.push("El documento no parece ser un DNI argentino");
|
|
53
|
+
if (!doc_number)
|
|
54
|
+
errors.push("No se pudo extraer número de DNI válido");
|
|
55
|
+
// TODO: extract full_name, date_of_birth, sex, expiry_date
|
|
56
|
+
return {
|
|
57
|
+
valid: errors.length === 0 && !!doc_number,
|
|
58
|
+
doc_number,
|
|
59
|
+
document_type: "dni",
|
|
60
|
+
country: "AR",
|
|
61
|
+
errors,
|
|
62
|
+
raw_ocr: ocrText,
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
validate(docNumber) {
|
|
66
|
+
const clean = docNumber.replace(/[.\s\-]/g, "");
|
|
67
|
+
if (!/^\d+$/.test(clean))
|
|
68
|
+
return { valid: false, error: "El DNI solo debe contener números" };
|
|
69
|
+
if (clean.length < 7 || clean.length > 8)
|
|
70
|
+
return { valid: false, error: `Longitud inválida: ${clean.length} dígitos (debe ser 7-8)` };
|
|
71
|
+
if (/^(\d)\1+$/.test(clean))
|
|
72
|
+
return { valid: false, error: "Número inválido (todos los dígitos iguales)" };
|
|
73
|
+
return { valid: true, normalized: clean };
|
|
74
|
+
},
|
|
75
|
+
parseMRZ(mrzText) {
|
|
76
|
+
// TODO: implement TD1 MRZ parser for DNI argentino
|
|
77
|
+
// Similar structure to Colombian cedula — see CO.ts
|
|
78
|
+
return {
|
|
79
|
+
valid: false,
|
|
80
|
+
country: "AR",
|
|
81
|
+
errors: ["MRZ parser para Argentina no implementado — contribuye en GitHub!"],
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
exports.default = AR;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🇧🇷 Brazil — RG (Registro Geral) & CPF
|
|
3
|
+
* ==========================================
|
|
4
|
+
* Supported documents:
|
|
5
|
+
* - RG (Registro Geral) — state-issued, format varies by state
|
|
6
|
+
* - CPF (Cadastro de Pessoas Físicas) — federal tax ID, 11 digits
|
|
7
|
+
* - CNH (Carteira Nacional de Habilitação) — driver's license
|
|
8
|
+
*
|
|
9
|
+
* Issuing authorities: Secretarias de Segurança Pública (RG), Receita Federal (CPF)
|
|
10
|
+
* CPF format: XXX.XXX.XXX-YY (11 digits, last 2 = check digits)
|
|
11
|
+
* CPF check: mod 11 algorithm (distinct from ICAO)
|
|
12
|
+
*
|
|
13
|
+
* Resources:
|
|
14
|
+
* - CPF: https://www.gov.br/receitafederal/pt-br
|
|
15
|
+
* - RG: varies by state
|
|
16
|
+
*
|
|
17
|
+
* 👋 CONTRIBUTOR: implement parse(), full CPF check digit validation
|
|
18
|
+
* Status: PARTIAL — CPF validation done, OCR parser is stub
|
|
19
|
+
*/
|
|
20
|
+
import type { CountryVerifier } from "../verifier.interface.js";
|
|
21
|
+
declare const BR: CountryVerifier;
|
|
22
|
+
export default BR;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 🇧🇷 Brazil — RG (Registro Geral) & CPF
|
|
4
|
+
* ==========================================
|
|
5
|
+
* Supported documents:
|
|
6
|
+
* - RG (Registro Geral) — state-issued, format varies by state
|
|
7
|
+
* - CPF (Cadastro de Pessoas Físicas) — federal tax ID, 11 digits
|
|
8
|
+
* - CNH (Carteira Nacional de Habilitação) — driver's license
|
|
9
|
+
*
|
|
10
|
+
* Issuing authorities: Secretarias de Segurança Pública (RG), Receita Federal (CPF)
|
|
11
|
+
* CPF format: XXX.XXX.XXX-YY (11 digits, last 2 = check digits)
|
|
12
|
+
* CPF check: mod 11 algorithm (distinct from ICAO)
|
|
13
|
+
*
|
|
14
|
+
* Resources:
|
|
15
|
+
* - CPF: https://www.gov.br/receitafederal/pt-br
|
|
16
|
+
* - RG: varies by state
|
|
17
|
+
*
|
|
18
|
+
* 👋 CONTRIBUTOR: implement parse(), full CPF check digit validation
|
|
19
|
+
* Status: PARTIAL — CPF validation done, OCR parser is stub
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
/**
|
|
23
|
+
* CPF check digit validation — mod 11 algorithm (Brazilian Receita Federal)
|
|
24
|
+
* Different from ICAO! Two check digits, each computed with decreasing weights.
|
|
25
|
+
*/
|
|
26
|
+
function validateCPF(cpf) {
|
|
27
|
+
const clean = cpf.replace(/[.\-\s]/g, "");
|
|
28
|
+
if (clean.length !== 11 || /^(\d)\1+$/.test(clean))
|
|
29
|
+
return false;
|
|
30
|
+
const calcDigit = (digits, len) => {
|
|
31
|
+
let sum = 0;
|
|
32
|
+
for (let i = 0; i < len; i++)
|
|
33
|
+
sum += parseInt(digits[i]) * (len + 1 - i);
|
|
34
|
+
const rem = (sum * 10) % 11;
|
|
35
|
+
return rem === 10 ? 0 : rem;
|
|
36
|
+
};
|
|
37
|
+
const d1 = calcDigit(clean, 9);
|
|
38
|
+
const d2 = calcDigit(clean, 10);
|
|
39
|
+
return parseInt(clean[9]) === d1 && parseInt(clean[10]) === d2;
|
|
40
|
+
}
|
|
41
|
+
const BR = {
|
|
42
|
+
countryCode: "BR",
|
|
43
|
+
countryName: "Brazil",
|
|
44
|
+
documentTypes: ["rg", "cpf", "cnh"],
|
|
45
|
+
parse(ocrText) {
|
|
46
|
+
const errors = [];
|
|
47
|
+
const text = ocrText.toUpperCase().replace(/\s+/g, " ").trim();
|
|
48
|
+
// Try CPF first (more reliable pattern)
|
|
49
|
+
let doc_number;
|
|
50
|
+
let document_type = "rg";
|
|
51
|
+
const cpfMatch = text.match(/CPF[:\s]*([0-9]{3}\.?[0-9]{3}\.?[0-9]{3}-?[0-9]{2})/);
|
|
52
|
+
if (cpfMatch) {
|
|
53
|
+
const v = BR.validate(cpfMatch[1]);
|
|
54
|
+
if (v.valid) {
|
|
55
|
+
doc_number = v.normalized;
|
|
56
|
+
document_type = "cpf";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// TODO: extract RG number, full_name, date_of_birth, sex
|
|
60
|
+
// RG format varies by issuing state (SP: X.XXX.XXX-X, MG: XXXXXXXXX, etc.)
|
|
61
|
+
const isBrazil = /BRASIL|BRAZIL|REPÚBLICA FEDERATIVA|FEDERATIVA DO BRASIL/i.test(text);
|
|
62
|
+
if (!isBrazil)
|
|
63
|
+
errors.push("O documento não parece ser brasileiro");
|
|
64
|
+
if (!doc_number)
|
|
65
|
+
errors.push("Não foi possível extrair CPF ou RG válido");
|
|
66
|
+
return {
|
|
67
|
+
valid: errors.length === 0 && !!doc_number,
|
|
68
|
+
doc_number,
|
|
69
|
+
document_type,
|
|
70
|
+
country: "BR",
|
|
71
|
+
errors,
|
|
72
|
+
raw_ocr: ocrText,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
validate(docNumber) {
|
|
76
|
+
const clean = docNumber.replace(/[.\-\s]/g, "");
|
|
77
|
+
// CPF: 11 digits
|
|
78
|
+
if (/^\d{11}$/.test(clean)) {
|
|
79
|
+
if (!validateCPF(clean))
|
|
80
|
+
return { valid: false, error: "CPF inválido (dígitos verificadores incorretos)" };
|
|
81
|
+
return { valid: true, normalized: clean };
|
|
82
|
+
}
|
|
83
|
+
// RG: 7-9 digits (state-dependent) — basic check only
|
|
84
|
+
if (/^\d{7,9}$/.test(clean))
|
|
85
|
+
return { valid: true, normalized: clean };
|
|
86
|
+
return { valid: false, error: "Não é um CPF (11 dígitos) nem RG válido (7-9 dígitos)" };
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
exports.default = BR;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🇨🇱 Chile — Cédula de Identidad (RUN/RUT)
|
|
3
|
+
* =============================================
|
|
4
|
+
* Supported documents:
|
|
5
|
+
* - Cédula de Identidad chilena — TD1 format
|
|
6
|
+
*
|
|
7
|
+
* Issuing authority: Servicio de Registro Civil e Identificación (SRCeI)
|
|
8
|
+
* Format: XXXXXXXX-Y (RUN: 7-8 digits + verifier digit, '0'-'9' or 'K')
|
|
9
|
+
* Check digit: mod 11 with weights 2,3,4,5,6,7 (right to left)
|
|
10
|
+
*
|
|
11
|
+
* Resources:
|
|
12
|
+
* - SRCeI: https://www.registrocivil.cl
|
|
13
|
+
* - RUN format: https://www.srcei.cl/run
|
|
14
|
+
*
|
|
15
|
+
* 👋 CONTRIBUTOR: implement parse(), parseMRZ()
|
|
16
|
+
* Status: PARTIAL — RUN validation done, OCR parser is stub
|
|
17
|
+
*/
|
|
18
|
+
import type { CountryVerifier } from "../verifier.interface.js";
|
|
19
|
+
declare const CL: CountryVerifier;
|
|
20
|
+
export default CL;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 🇨🇱 Chile — Cédula de Identidad (RUN/RUT)
|
|
4
|
+
* =============================================
|
|
5
|
+
* Supported documents:
|
|
6
|
+
* - Cédula de Identidad chilena — TD1 format
|
|
7
|
+
*
|
|
8
|
+
* Issuing authority: Servicio de Registro Civil e Identificación (SRCeI)
|
|
9
|
+
* Format: XXXXXXXX-Y (RUN: 7-8 digits + verifier digit, '0'-'9' or 'K')
|
|
10
|
+
* Check digit: mod 11 with weights 2,3,4,5,6,7 (right to left)
|
|
11
|
+
*
|
|
12
|
+
* Resources:
|
|
13
|
+
* - SRCeI: https://www.registrocivil.cl
|
|
14
|
+
* - RUN format: https://www.srcei.cl/run
|
|
15
|
+
*
|
|
16
|
+
* 👋 CONTRIBUTOR: implement parse(), parseMRZ()
|
|
17
|
+
* Status: PARTIAL — RUN validation done, OCR parser is stub
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
/** Chile RUN check digit — mod 11, returns '0'-'9' or 'K' */
|
|
21
|
+
function runCheckDigit(num) {
|
|
22
|
+
const digits = num.replace(/\./g, "").split("").reverse();
|
|
23
|
+
const weights = [2, 3, 4, 5, 6, 7];
|
|
24
|
+
let sum = 0;
|
|
25
|
+
digits.forEach((d, i) => { sum += parseInt(d) * weights[i % weights.length]; });
|
|
26
|
+
const rem = 11 - (sum % 11);
|
|
27
|
+
if (rem === 11)
|
|
28
|
+
return "0";
|
|
29
|
+
if (rem === 10)
|
|
30
|
+
return "K";
|
|
31
|
+
return rem.toString();
|
|
32
|
+
}
|
|
33
|
+
const CL = {
|
|
34
|
+
countryCode: "CL",
|
|
35
|
+
countryName: "Chile",
|
|
36
|
+
documentTypes: ["cedula_identidad", "rut"],
|
|
37
|
+
parse(ocrText) {
|
|
38
|
+
const errors = [];
|
|
39
|
+
const text = ocrText.toUpperCase().replace(/\s+/g, " ").trim();
|
|
40
|
+
let doc_number;
|
|
41
|
+
const runPatterns = [
|
|
42
|
+
/RUN[:\s]*(\d{1,2}\.?\d{3}\.?\d{3}-?[0-9K])/i,
|
|
43
|
+
/RUT[:\s]*(\d{1,2}\.?\d{3}\.?\d{3}-?[0-9K])/i,
|
|
44
|
+
/(\d{1,2}\.?\d{3}\.?\d{3}-[0-9K])/,
|
|
45
|
+
];
|
|
46
|
+
for (const pat of runPatterns) {
|
|
47
|
+
const m = text.match(pat);
|
|
48
|
+
if (m) {
|
|
49
|
+
const v = CL.validate(m[1]);
|
|
50
|
+
if (v.valid) {
|
|
51
|
+
doc_number = v.normalized;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const isChile = /CHILE|REGISTRO CIVIL|REPÚBLICA DE CHILE/i.test(text);
|
|
57
|
+
if (!isChile)
|
|
58
|
+
errors.push("El documento no parece ser una cédula chilena");
|
|
59
|
+
if (!doc_number)
|
|
60
|
+
errors.push("No se pudo extraer RUN válido");
|
|
61
|
+
// TODO: extract full_name, date_of_birth, sex, expiry_date
|
|
62
|
+
return {
|
|
63
|
+
valid: errors.length === 0 && !!doc_number,
|
|
64
|
+
doc_number,
|
|
65
|
+
document_type: "cedula_identidad",
|
|
66
|
+
country: "CL",
|
|
67
|
+
errors,
|
|
68
|
+
raw_ocr: ocrText,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
validate(docNumber) {
|
|
72
|
+
const clean = docNumber.replace(/[.\s]/g, "").toUpperCase();
|
|
73
|
+
const m = clean.match(/^(\d{7,8})-?([0-9K])$/);
|
|
74
|
+
if (!m)
|
|
75
|
+
return { valid: false, error: "Formato inválido. Debe ser XXXXXXXX-Y (dígito verificador 0-9 o K)" };
|
|
76
|
+
const expected = runCheckDigit(m[1]);
|
|
77
|
+
if (m[2] !== expected)
|
|
78
|
+
return { valid: false, error: `Dígito verificador incorrecto (esperado: ${expected}, encontrado: ${m[2]})` };
|
|
79
|
+
return { valid: true, normalized: `${m[1]}-${m[2]}` };
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
exports.default = CL;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🇨🇴 Colombia — Cédula de Ciudadanía & Cédula de Extranjería
|
|
3
|
+
* ================================================================
|
|
4
|
+
* Supported documents:
|
|
5
|
+
* - Cédula de Ciudadanía (CC) — TD1 format, MRZ on back
|
|
6
|
+
* - Cédula de Extranjería (CE) — for foreign residents
|
|
7
|
+
*
|
|
8
|
+
* Issuing authority: Registraduría Nacional del Estado Civil
|
|
9
|
+
* Format: 5–10 numeric digits
|
|
10
|
+
* MRZ: TD1 (3 lines × 30 chars), ICAO 9303
|
|
11
|
+
*
|
|
12
|
+
* Contributor: @manuelariasfz
|
|
13
|
+
*/
|
|
14
|
+
import type { CountryVerifier } from "../verifier.interface.js";
|
|
15
|
+
declare const CO: CountryVerifier;
|
|
16
|
+
export default CO;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 🇨🇴 Colombia — Cédula de Ciudadanía & Cédula de Extranjería
|
|
4
|
+
* ================================================================
|
|
5
|
+
* Supported documents:
|
|
6
|
+
* - Cédula de Ciudadanía (CC) — TD1 format, MRZ on back
|
|
7
|
+
* - Cédula de Extranjería (CE) — for foreign residents
|
|
8
|
+
*
|
|
9
|
+
* Issuing authority: Registraduría Nacional del Estado Civil
|
|
10
|
+
* Format: 5–10 numeric digits
|
|
11
|
+
* MRZ: TD1 (3 lines × 30 chars), ICAO 9303
|
|
12
|
+
*
|
|
13
|
+
* Contributor: @manuelariasfz
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const cedula_validator_js_1 = require("../cedula-validator.js");
|
|
17
|
+
const CO = {
|
|
18
|
+
countryCode: "CO",
|
|
19
|
+
countryName: "Colombia",
|
|
20
|
+
documentTypes: ["cedula", "cedula_extranjeria"],
|
|
21
|
+
parse(ocrText) {
|
|
22
|
+
const r = (0, cedula_validator_js_1.parseCedulaOCR)(ocrText);
|
|
23
|
+
return {
|
|
24
|
+
valid: r.valid,
|
|
25
|
+
doc_number: r.cedula_number,
|
|
26
|
+
full_name: r.nombre,
|
|
27
|
+
date_of_birth: r.fecha_nacimiento,
|
|
28
|
+
sex: r.sexo,
|
|
29
|
+
document_type: "cedula",
|
|
30
|
+
country: "CO",
|
|
31
|
+
errors: r.errors,
|
|
32
|
+
raw_ocr: r.raw_ocr,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
validate(docNumber) {
|
|
36
|
+
const r = (0, cedula_validator_js_1.validateCedulaNumber)(docNumber);
|
|
37
|
+
if (!r.valid)
|
|
38
|
+
return { valid: false, error: r.error };
|
|
39
|
+
const normalized = docNumber.replace(/[\s\-\.]/g, "");
|
|
40
|
+
return { valid: true, normalized };
|
|
41
|
+
},
|
|
42
|
+
parseMRZ(mrzText) {
|
|
43
|
+
const r = (0, cedula_validator_js_1.parseMRZ)(mrzText);
|
|
44
|
+
return {
|
|
45
|
+
valid: r.valid,
|
|
46
|
+
doc_number: r.cedula_number,
|
|
47
|
+
full_name: r.nombre,
|
|
48
|
+
date_of_birth: r.fecha_nacimiento,
|
|
49
|
+
sex: r.sexo,
|
|
50
|
+
document_type: "cedula",
|
|
51
|
+
country: "CO",
|
|
52
|
+
errors: r.errors,
|
|
53
|
+
raw_ocr: r.raw_ocr,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
async quickValidate(imagePath) {
|
|
57
|
+
try {
|
|
58
|
+
const sharp = (await import("sharp")).default;
|
|
59
|
+
const meta = await sharp(imagePath).metadata();
|
|
60
|
+
if (!meta.width || !meta.height)
|
|
61
|
+
return { valid: false, error: "No se pudo leer dimensiones" };
|
|
62
|
+
const ratio = meta.width / meta.height;
|
|
63
|
+
const isLandscape = meta.width > meta.height;
|
|
64
|
+
if (!isLandscape)
|
|
65
|
+
return { valid: false, error: "La cédula debe estar en horizontal" };
|
|
66
|
+
if (ratio < 1.2 || ratio > 2.0)
|
|
67
|
+
return { valid: false, error: `Proporción inusual (${ratio.toFixed(2)}) — fotografia solo la cédula` };
|
|
68
|
+
if (meta.width < 400 || meta.height < 250)
|
|
69
|
+
return { valid: false, error: "Imagen muy pequeña — usa al menos 400×250px" };
|
|
70
|
+
return { valid: true };
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return { valid: false, error: `Error leyendo imagen: ${e.message}` };
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
exports.default = CO;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🇲🇽 Mexico — Credencial para Votar (INE/IFE) & Pasaporte
|
|
3
|
+
* =============================================================
|
|
4
|
+
* Supported documents:
|
|
5
|
+
* - Credencial para Votar (INE) — issued by Instituto Nacional Electoral
|
|
6
|
+
* - Pasaporte mexicano — ICAO TD3
|
|
7
|
+
*
|
|
8
|
+
* Key fields on INE:
|
|
9
|
+
* - CURP: 18-char alphanumeric (Clave Única de Registro de Población)
|
|
10
|
+
* - Clave de elector: 18 chars
|
|
11
|
+
* - Número de emisión: 2 digits
|
|
12
|
+
*
|
|
13
|
+
* Resources:
|
|
14
|
+
* - CURP format: https://www.gob.mx/curp
|
|
15
|
+
* - INE layout: https://www.ine.mx/credencial/
|
|
16
|
+
*
|
|
17
|
+
* 👋 CONTRIBUTOR: implement parse(), validate(), parseMRZ()
|
|
18
|
+
* Status: STUB — contributions welcome!
|
|
19
|
+
*/
|
|
20
|
+
import type { CountryVerifier } from "../verifier.interface.js";
|
|
21
|
+
declare const MX: CountryVerifier;
|
|
22
|
+
export default MX;
|