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.
@@ -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] = dígito verificador
59
+ * - [6] = check digit fecha nac
31
60
  * - [7] = sexo M/F
32
61
  * - [8-13] = fecha expiración YYMMDD
33
- * - [14] = dígito verificador
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] = dígito verificador
183
+ * - [6] = check digit fecha nac
138
184
  * - [7] = sexo M/F
139
185
  * - [8-13] = fecha expiración YYMMDD
140
- * - [14] = dígito verificador
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 2: datos biográficos
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
- // Inferir siglo (si YY > 24 19xx, si <= 24 → 20xx)
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: aparece en línea 2 posición 18-27 (o en línea 1)
170
- const docNumRaw = line2.slice(18, 29).replace(/</g, "").trim();
171
- const docNum = docNumRaw.replace(/^0+/, ""); // quitar ceros a la izquierda
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
- // Línea 3: nombre — buscar en TODAS las líneas (puede ser más corta)
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 ? `${nombres} ${apellido}`.trim() : (apellido || nombres);
183
- }
184
- if (!numValidation.valid) {
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;