@openleash/core 0.3.0 → 0.5.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/dist/identity-validators.d.ts +26 -0
- package/dist/identity-validators.d.ts.map +1 -0
- package/dist/identity-validators.js +664 -0
- package/dist/identity-validators.js.map +1 -0
- package/dist/identity.d.ts +250 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +244 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/passphrase.d.ts +6 -0
- package/dist/passphrase.d.ts.map +1 -0
- package/dist/passphrase.js +61 -0
- package/dist/passphrase.js.map +1 -0
- package/dist/state.d.ts +10 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +65 -0
- package/dist/state.js.map +1 -1
- package/dist/tokens.d.ts +35 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +82 -0
- package/dist/tokens.js.map +1 -1
- package/dist/totp.d.ts +15 -0
- package/dist/totp.d.ts.map +1 -0
- package/dist/totp.js +149 -0
- package/dist/totp.js.map +1 -0
- package/dist/types.d.ts +227 -75
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +36 -3
- package/dist/types.js.map +1 -1
- package/package.json +8 -4
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ValidationResult {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
error?: string;
|
|
4
|
+
}
|
|
5
|
+
/** Luhn algorithm (ISO/IEC 7812). Returns true if check digit is valid. */
|
|
6
|
+
export declare function luhn(digits: string): boolean;
|
|
7
|
+
/** ISO 7064 Mod 11,10 check digit algorithm. */
|
|
8
|
+
export declare function iso7064Mod11_10(digits: string): boolean;
|
|
9
|
+
/** Mod 97 on a numeric string. Returns the remainder. */
|
|
10
|
+
export declare function mod97(numStr: string): number;
|
|
11
|
+
/** Mod 97-10 (ISO 7064) for alphanumeric strings (used by LEI, IBAN). */
|
|
12
|
+
export declare function mod97_10(alphanumeric: string): boolean;
|
|
13
|
+
export declare const EU_PERSONAL_ID_VALIDATORS: Record<string, (value: string) => ValidationResult>;
|
|
14
|
+
/** List of known personal ID types per country. */
|
|
15
|
+
export declare const EU_PERSONAL_ID_TYPES: Record<string, string[]>;
|
|
16
|
+
/** Validate a VAT number. Value should include the country prefix (e.g. "SE556123456701"). */
|
|
17
|
+
export declare function validateVAT(value: string): ValidationResult;
|
|
18
|
+
/** Validate a company registration number for a given country. */
|
|
19
|
+
export declare function validateCompanyReg(value: string, country: string): ValidationResult;
|
|
20
|
+
/** Validate a Legal Entity Identifier (ISO 17442). 20 alphanumeric chars, mod 97-10. */
|
|
21
|
+
export declare function validateLEI(value: string): ValidationResult;
|
|
22
|
+
/** Validate a DUNS number. 9 digits. */
|
|
23
|
+
export declare function validateDUNS(value: string): ValidationResult;
|
|
24
|
+
/** Validate an EORI number. Country prefix + up to 15 alphanumeric chars. */
|
|
25
|
+
export declare function validateEORI(value: string): ValidationResult;
|
|
26
|
+
//# sourceMappingURL=identity-validators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity-validators.d.ts","sourceRoot":"","sources":["../src/identity-validators.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,2EAA2E;AAC3E,wBAAgB,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAe5C;AAED,gDAAgD;AAChD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAWvD;AAED,yDAAyD;AACzD,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAM5C;AAED,yEAAyE;AACzE,wBAAgB,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CActD;AAwZD,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,gBAAgB,CAmCzF,CAAC;AAEF,mDAAmD;AACnD,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CA4BzD,CAAC;AAmCF,8FAA8F;AAC9F,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAgB3D;AAiCD,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAgBnF;AAED,wFAAwF;AACxF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAS3D;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAM5D;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAM5D"}
|
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Identity validation algorithms ─────────────────────────────────
|
|
3
|
+
// All checksum and format validation for EU personal IDs, company IDs,
|
|
4
|
+
// VAT numbers, LEI, DUNS, and EORI. No external dependencies.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.EU_PERSONAL_ID_TYPES = exports.EU_PERSONAL_ID_VALIDATORS = void 0;
|
|
7
|
+
exports.luhn = luhn;
|
|
8
|
+
exports.iso7064Mod11_10 = iso7064Mod11_10;
|
|
9
|
+
exports.mod97 = mod97;
|
|
10
|
+
exports.mod97_10 = mod97_10;
|
|
11
|
+
exports.validateVAT = validateVAT;
|
|
12
|
+
exports.validateCompanyReg = validateCompanyReg;
|
|
13
|
+
exports.validateLEI = validateLEI;
|
|
14
|
+
exports.validateDUNS = validateDUNS;
|
|
15
|
+
exports.validateEORI = validateEORI;
|
|
16
|
+
const ok = { valid: true };
|
|
17
|
+
const fail = (error) => ({ valid: false, error });
|
|
18
|
+
// ─── Shared algorithms ─────────────────────────────────────────────
|
|
19
|
+
/** Luhn algorithm (ISO/IEC 7812). Returns true if check digit is valid. */
|
|
20
|
+
function luhn(digits) {
|
|
21
|
+
const nums = digits.split('').map(Number);
|
|
22
|
+
if (nums.some(isNaN))
|
|
23
|
+
return false;
|
|
24
|
+
let sum = 0;
|
|
25
|
+
let alt = false;
|
|
26
|
+
for (let i = nums.length - 1; i >= 0; i--) {
|
|
27
|
+
let n = nums[i];
|
|
28
|
+
if (alt) {
|
|
29
|
+
n *= 2;
|
|
30
|
+
if (n > 9)
|
|
31
|
+
n -= 9;
|
|
32
|
+
}
|
|
33
|
+
sum += n;
|
|
34
|
+
alt = !alt;
|
|
35
|
+
}
|
|
36
|
+
return sum % 10 === 0;
|
|
37
|
+
}
|
|
38
|
+
/** ISO 7064 Mod 11,10 check digit algorithm. */
|
|
39
|
+
function iso7064Mod11_10(digits) {
|
|
40
|
+
const nums = digits.split('').map(Number);
|
|
41
|
+
if (nums.some(isNaN) || nums.length < 2)
|
|
42
|
+
return false;
|
|
43
|
+
let remainder = 10;
|
|
44
|
+
for (let i = 0; i < nums.length - 1; i++) {
|
|
45
|
+
let sum = (nums[i] + remainder) % 10;
|
|
46
|
+
if (sum === 0)
|
|
47
|
+
sum = 10;
|
|
48
|
+
remainder = (sum * 2) % 11;
|
|
49
|
+
}
|
|
50
|
+
const checkDigit = (11 - remainder) % 10;
|
|
51
|
+
return checkDigit === nums[nums.length - 1];
|
|
52
|
+
}
|
|
53
|
+
/** Mod 97 on a numeric string. Returns the remainder. */
|
|
54
|
+
function mod97(numStr) {
|
|
55
|
+
let remainder = 0;
|
|
56
|
+
for (let i = 0; i < numStr.length; i++) {
|
|
57
|
+
remainder = (remainder * 10 + parseInt(numStr[i], 10)) % 97;
|
|
58
|
+
}
|
|
59
|
+
return remainder;
|
|
60
|
+
}
|
|
61
|
+
/** Mod 97-10 (ISO 7064) for alphanumeric strings (used by LEI, IBAN). */
|
|
62
|
+
function mod97_10(alphanumeric) {
|
|
63
|
+
// Convert letters to numbers: A=10, B=11, ..., Z=35
|
|
64
|
+
let numStr = '';
|
|
65
|
+
for (const ch of alphanumeric.toUpperCase()) {
|
|
66
|
+
const code = ch.charCodeAt(0);
|
|
67
|
+
if (code >= 48 && code <= 57) {
|
|
68
|
+
numStr += ch;
|
|
69
|
+
}
|
|
70
|
+
else if (code >= 65 && code <= 90) {
|
|
71
|
+
numStr += (code - 55).toString();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return mod97(numStr) === 1;
|
|
78
|
+
}
|
|
79
|
+
function makeRegexValidator(pattern, description) {
|
|
80
|
+
return (value) => pattern.test(value) ? ok : fail(`Invalid format: expected ${description}`);
|
|
81
|
+
}
|
|
82
|
+
// ─── Personal ID validators ────────────────────────────────────────
|
|
83
|
+
/** Sweden: Personnummer (YYMMDD-XXXX or YYYYMMDD-XXXX) */
|
|
84
|
+
function validateSE_PERSONNUMMER(value) {
|
|
85
|
+
// Normalize: remove hyphens and accept both 10 and 12 digit formats
|
|
86
|
+
const cleaned = value.replace(/[-+]/g, '');
|
|
87
|
+
let digits10;
|
|
88
|
+
if (cleaned.length === 12) {
|
|
89
|
+
digits10 = cleaned.slice(2);
|
|
90
|
+
}
|
|
91
|
+
else if (cleaned.length === 10) {
|
|
92
|
+
digits10 = cleaned;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
return fail('Personnummer must be 10 or 12 digits (YYMMDD-XXXX or YYYYMMDD-XXXX)');
|
|
96
|
+
}
|
|
97
|
+
if (!/^\d{10}$/.test(digits10)) {
|
|
98
|
+
return fail('Personnummer must contain only digits (and optional hyphen)');
|
|
99
|
+
}
|
|
100
|
+
if (!luhn(digits10)) {
|
|
101
|
+
return fail('Invalid Luhn check digit');
|
|
102
|
+
}
|
|
103
|
+
return ok;
|
|
104
|
+
}
|
|
105
|
+
/** Netherlands: BSN (Burgerservicenummer) — 9 digits, 11-test */
|
|
106
|
+
function validateNL_BSN(value) {
|
|
107
|
+
const cleaned = value.replace(/\s/g, '');
|
|
108
|
+
if (!/^\d{9}$/.test(cleaned)) {
|
|
109
|
+
return fail('BSN must be exactly 9 digits');
|
|
110
|
+
}
|
|
111
|
+
const d = cleaned.split('').map(Number);
|
|
112
|
+
// 11-test: 9*d[0] + 8*d[1] + ... + 2*d[7] - 1*d[8] must be divisible by 11
|
|
113
|
+
const sum = 9 * d[0] + 8 * d[1] + 7 * d[2] + 6 * d[3] + 5 * d[4] + 4 * d[5] + 3 * d[6] + 2 * d[7] - 1 * d[8];
|
|
114
|
+
if (sum % 11 !== 0 || sum === 0) {
|
|
115
|
+
return fail('Invalid BSN check (11-test failed)');
|
|
116
|
+
}
|
|
117
|
+
return ok;
|
|
118
|
+
}
|
|
119
|
+
/** Belgium: Rijksregisternummer — 11 digits, mod 97 check */
|
|
120
|
+
function validateBE_RRN(value) {
|
|
121
|
+
const cleaned = value.replace(/[.\-\s]/g, '');
|
|
122
|
+
if (!/^\d{11}$/.test(cleaned)) {
|
|
123
|
+
return fail('Rijksregisternummer must be exactly 11 digits');
|
|
124
|
+
}
|
|
125
|
+
const first9 = parseInt(cleaned.slice(0, 9), 10);
|
|
126
|
+
const checkDigits = parseInt(cleaned.slice(9, 11), 10);
|
|
127
|
+
// For people born before 2000
|
|
128
|
+
if (97 - (first9 % 97) === checkDigits)
|
|
129
|
+
return ok;
|
|
130
|
+
// For people born in 2000 or later, prefix with '2'
|
|
131
|
+
const first9With2 = parseInt('2' + cleaned.slice(0, 9), 10);
|
|
132
|
+
if (97 - (first9With2 % 97) === checkDigits)
|
|
133
|
+
return ok;
|
|
134
|
+
return fail('Invalid Rijksregisternummer check digits (mod 97)');
|
|
135
|
+
}
|
|
136
|
+
/** Poland: PESEL — 11 digits, weighted checksum */
|
|
137
|
+
function validatePL_PESEL(value) {
|
|
138
|
+
const cleaned = value.replace(/\s/g, '');
|
|
139
|
+
if (!/^\d{11}$/.test(cleaned)) {
|
|
140
|
+
return fail('PESEL must be exactly 11 digits');
|
|
141
|
+
}
|
|
142
|
+
const d = cleaned.split('').map(Number);
|
|
143
|
+
const weights = [1, 3, 7, 9, 1, 3, 7, 9, 1, 3];
|
|
144
|
+
let sum = 0;
|
|
145
|
+
for (let i = 0; i < 10; i++) {
|
|
146
|
+
sum += d[i] * weights[i];
|
|
147
|
+
}
|
|
148
|
+
const check = (10 - (sum % 10)) % 10;
|
|
149
|
+
if (check !== d[10]) {
|
|
150
|
+
return fail('Invalid PESEL check digit');
|
|
151
|
+
}
|
|
152
|
+
return ok;
|
|
153
|
+
}
|
|
154
|
+
/** Finland: Henkilötunnus — DDMMYY{-+A}NNNC */
|
|
155
|
+
function validateFI_HETU(value) {
|
|
156
|
+
const match = value.match(/^(\d{6})([+\-ABCDEFYXWVU])(\d{3})([0-9A-Z])$/i);
|
|
157
|
+
if (!match) {
|
|
158
|
+
return fail('Henkilötunnus must match DDMMYY{separator}NNNC format');
|
|
159
|
+
}
|
|
160
|
+
const datePart = match[1];
|
|
161
|
+
const individualNumber = match[3];
|
|
162
|
+
const checkChar = match[4].toUpperCase();
|
|
163
|
+
const remainder = parseInt(datePart + individualNumber, 10) % 31;
|
|
164
|
+
const checkChars = '0123456789ABCDEFHJKLMNPRSTUVWXY';
|
|
165
|
+
if (checkChars[remainder] !== checkChar) {
|
|
166
|
+
return fail('Invalid Henkilötunnus check character');
|
|
167
|
+
}
|
|
168
|
+
return ok;
|
|
169
|
+
}
|
|
170
|
+
/** Spain: DNI — 8 digits + letter */
|
|
171
|
+
function validateES_DNI(value) {
|
|
172
|
+
const cleaned = value.replace(/[\s-]/g, '').toUpperCase();
|
|
173
|
+
const match = cleaned.match(/^(\d{8})([A-Z])$/);
|
|
174
|
+
if (!match) {
|
|
175
|
+
return fail('DNI must be 8 digits followed by a letter');
|
|
176
|
+
}
|
|
177
|
+
const num = parseInt(match[1], 10);
|
|
178
|
+
const letter = match[2];
|
|
179
|
+
const letters = 'TRWAGMYFPDXBNJZSQVHLCKE';
|
|
180
|
+
if (letters[num % 23] !== letter) {
|
|
181
|
+
return fail('Invalid DNI check letter');
|
|
182
|
+
}
|
|
183
|
+
return ok;
|
|
184
|
+
}
|
|
185
|
+
/** Spain: NIE — X/Y/Z + 7 digits + letter */
|
|
186
|
+
function validateES_NIE(value) {
|
|
187
|
+
const cleaned = value.replace(/[\s-]/g, '').toUpperCase();
|
|
188
|
+
const match = cleaned.match(/^([XYZ])(\d{7})([A-Z])$/);
|
|
189
|
+
if (!match) {
|
|
190
|
+
return fail('NIE must be X/Y/Z followed by 7 digits and a letter');
|
|
191
|
+
}
|
|
192
|
+
const prefixMap = { X: '0', Y: '1', Z: '2' };
|
|
193
|
+
const num = parseInt(prefixMap[match[1]] + match[2], 10);
|
|
194
|
+
const letter = match[3];
|
|
195
|
+
const letters = 'TRWAGMYFPDXBNJZSQVHLCKE';
|
|
196
|
+
if (letters[num % 23] !== letter) {
|
|
197
|
+
return fail('Invalid NIE check letter');
|
|
198
|
+
}
|
|
199
|
+
return ok;
|
|
200
|
+
}
|
|
201
|
+
/** Italy: Codice Fiscale — 16 alphanumeric chars */
|
|
202
|
+
function validateIT_CF(value) {
|
|
203
|
+
const cleaned = value.replace(/\s/g, '').toUpperCase();
|
|
204
|
+
if (!/^[A-Z0-9]{16}$/.test(cleaned)) {
|
|
205
|
+
return fail('Codice Fiscale must be exactly 16 alphanumeric characters');
|
|
206
|
+
}
|
|
207
|
+
const oddValues = {
|
|
208
|
+
'0': 1, '1': 0, '2': 5, '3': 7, '4': 9, '5': 13, '6': 15, '7': 17, '8': 19, '9': 21,
|
|
209
|
+
'A': 1, 'B': 0, 'C': 5, 'D': 7, 'E': 9, 'F': 13, 'G': 15, 'H': 17, 'I': 19, 'J': 21,
|
|
210
|
+
'K': 2, 'L': 4, 'M': 18, 'N': 20, 'O': 11, 'P': 3, 'Q': 6, 'R': 8, 'S': 12, 'T': 14,
|
|
211
|
+
'U': 16, 'V': 10, 'W': 22, 'X': 25, 'Y': 24, 'Z': 23,
|
|
212
|
+
};
|
|
213
|
+
const evenValues = {
|
|
214
|
+
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
|
|
215
|
+
'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'I': 8, 'J': 9,
|
|
216
|
+
'K': 10, 'L': 11, 'M': 12, 'N': 13, 'O': 14, 'P': 15, 'Q': 16, 'R': 17, 'S': 18, 'T': 19,
|
|
217
|
+
'U': 20, 'V': 21, 'W': 22, 'X': 23, 'Y': 24, 'Z': 25,
|
|
218
|
+
};
|
|
219
|
+
let sum = 0;
|
|
220
|
+
for (let i = 0; i < 15; i++) {
|
|
221
|
+
const ch = cleaned[i];
|
|
222
|
+
// Positions are 1-indexed: odd positions (1,3,5,...) use odd table
|
|
223
|
+
if ((i + 1) % 2 === 1) {
|
|
224
|
+
sum += oddValues[ch] ?? 0;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
sum += evenValues[ch] ?? 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const expectedCheck = String.fromCharCode(65 + (sum % 26));
|
|
231
|
+
if (cleaned[15] !== expectedCheck) {
|
|
232
|
+
return fail('Invalid Codice Fiscale check character');
|
|
233
|
+
}
|
|
234
|
+
return ok;
|
|
235
|
+
}
|
|
236
|
+
/** Germany: Steuerliche Identifikationsnummer — 11 digits, ISO 7064 Mod 11,10 */
|
|
237
|
+
function validateDE_STEUERID(value) {
|
|
238
|
+
const cleaned = value.replace(/[\s-]/g, '');
|
|
239
|
+
if (!/^\d{11}$/.test(cleaned)) {
|
|
240
|
+
return fail('Steuer-ID must be exactly 11 digits');
|
|
241
|
+
}
|
|
242
|
+
// First digit must not be 0
|
|
243
|
+
if (cleaned[0] === '0') {
|
|
244
|
+
return fail('Steuer-ID must not start with 0');
|
|
245
|
+
}
|
|
246
|
+
if (!iso7064Mod11_10(cleaned)) {
|
|
247
|
+
return fail('Invalid Steuer-ID check digit');
|
|
248
|
+
}
|
|
249
|
+
return ok;
|
|
250
|
+
}
|
|
251
|
+
/** France: NIR (Numéro de sécurité sociale) — 13 digits + 2-digit key */
|
|
252
|
+
function validateFR_NIR(value) {
|
|
253
|
+
// Accept with or without spaces/dashes, and with the 2-digit key
|
|
254
|
+
let cleaned = value.replace(/[\s.-]/g, '');
|
|
255
|
+
// Handle Corsica: 2A → 19, 2B → 18
|
|
256
|
+
cleaned = cleaned.replace(/^(\d)2A/i, '$119').replace(/^(\d)2B/i, '$118');
|
|
257
|
+
if (!/^\d{15}$/.test(cleaned)) {
|
|
258
|
+
return fail('NIR must be 13 digits + 2-digit key (15 total)');
|
|
259
|
+
}
|
|
260
|
+
const mainPart = cleaned.slice(0, 13);
|
|
261
|
+
const key = parseInt(cleaned.slice(13, 15), 10);
|
|
262
|
+
// Use BigInt for precision with 13-digit numbers
|
|
263
|
+
const mainNum = BigInt(mainPart);
|
|
264
|
+
const expectedKey = 97 - Number(mainNum % 97n);
|
|
265
|
+
if (key !== expectedKey) {
|
|
266
|
+
return fail('Invalid NIR key digits');
|
|
267
|
+
}
|
|
268
|
+
return ok;
|
|
269
|
+
}
|
|
270
|
+
/** Croatia: OIB — 11 digits, ISO 7064 Mod 11,10 */
|
|
271
|
+
function validateHR_OIB(value) {
|
|
272
|
+
const cleaned = value.replace(/\s/g, '');
|
|
273
|
+
if (!/^\d{11}$/.test(cleaned)) {
|
|
274
|
+
return fail('OIB must be exactly 11 digits');
|
|
275
|
+
}
|
|
276
|
+
if (!iso7064Mod11_10(cleaned)) {
|
|
277
|
+
return fail('Invalid OIB check digit');
|
|
278
|
+
}
|
|
279
|
+
return ok;
|
|
280
|
+
}
|
|
281
|
+
/** Bulgaria: EGN — 10 digits, weighted checksum */
|
|
282
|
+
function validateBG_EGN(value) {
|
|
283
|
+
const cleaned = value.replace(/\s/g, '');
|
|
284
|
+
if (!/^\d{10}$/.test(cleaned)) {
|
|
285
|
+
return fail('EGN must be exactly 10 digits');
|
|
286
|
+
}
|
|
287
|
+
const d = cleaned.split('').map(Number);
|
|
288
|
+
const weights = [2, 4, 8, 5, 10, 9, 7, 3, 6];
|
|
289
|
+
let sum = 0;
|
|
290
|
+
for (let i = 0; i < 9; i++) {
|
|
291
|
+
sum += d[i] * weights[i];
|
|
292
|
+
}
|
|
293
|
+
const check = sum % 11;
|
|
294
|
+
const expected = check === 10 ? 0 : check;
|
|
295
|
+
if (expected !== d[9]) {
|
|
296
|
+
return fail('Invalid EGN check digit');
|
|
297
|
+
}
|
|
298
|
+
return ok;
|
|
299
|
+
}
|
|
300
|
+
/** Czech Republic: Rodné číslo — 9 or 10 digits */
|
|
301
|
+
function validateCZ_RC(value) {
|
|
302
|
+
const cleaned = value.replace(/[/\s]/g, '');
|
|
303
|
+
if (!/^\d{9,10}$/.test(cleaned)) {
|
|
304
|
+
return fail('Rodné číslo must be 9 or 10 digits');
|
|
305
|
+
}
|
|
306
|
+
if (cleaned.length === 10) {
|
|
307
|
+
const num = parseInt(cleaned, 10);
|
|
308
|
+
if (num % 11 !== 0) {
|
|
309
|
+
return fail('Invalid Rodné číslo (10-digit must be divisible by 11)');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return ok;
|
|
313
|
+
}
|
|
314
|
+
/** Denmark: CPR-nummer — 10 digits (DDMMYY-XXXX) */
|
|
315
|
+
function validateDK_CPR(value) {
|
|
316
|
+
const cleaned = value.replace(/[\s-]/g, '');
|
|
317
|
+
if (!/^\d{10}$/.test(cleaned)) {
|
|
318
|
+
return fail('CPR must be exactly 10 digits');
|
|
319
|
+
}
|
|
320
|
+
// Mod-11 check was removed by Danish authorities in 2007, so we only validate format
|
|
321
|
+
return ok;
|
|
322
|
+
}
|
|
323
|
+
/** Estonia: Isikukood — 11 digits */
|
|
324
|
+
function validateEE_IK(value) {
|
|
325
|
+
const cleaned = value.replace(/\s/g, '');
|
|
326
|
+
if (!/^\d{11}$/.test(cleaned)) {
|
|
327
|
+
return fail('Isikukood must be exactly 11 digits');
|
|
328
|
+
}
|
|
329
|
+
const d = cleaned.split('').map(Number);
|
|
330
|
+
const weights1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1];
|
|
331
|
+
let sum = 0;
|
|
332
|
+
for (let i = 0; i < 10; i++)
|
|
333
|
+
sum += d[i] * weights1[i];
|
|
334
|
+
let check = sum % 11;
|
|
335
|
+
if (check === 10) {
|
|
336
|
+
const weights2 = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3];
|
|
337
|
+
sum = 0;
|
|
338
|
+
for (let i = 0; i < 10; i++)
|
|
339
|
+
sum += d[i] * weights2[i];
|
|
340
|
+
check = sum % 11;
|
|
341
|
+
if (check === 10)
|
|
342
|
+
check = 0;
|
|
343
|
+
}
|
|
344
|
+
if (check !== d[10]) {
|
|
345
|
+
return fail('Invalid Isikukood check digit');
|
|
346
|
+
}
|
|
347
|
+
return ok;
|
|
348
|
+
}
|
|
349
|
+
/** Greece: AMKA — 11 digits, Luhn */
|
|
350
|
+
function validateGR_AMKA(value) {
|
|
351
|
+
const cleaned = value.replace(/\s/g, '');
|
|
352
|
+
if (!/^\d{11}$/.test(cleaned)) {
|
|
353
|
+
return fail('AMKA must be exactly 11 digits');
|
|
354
|
+
}
|
|
355
|
+
if (!luhn(cleaned)) {
|
|
356
|
+
return fail('Invalid AMKA check digit (Luhn)');
|
|
357
|
+
}
|
|
358
|
+
return ok;
|
|
359
|
+
}
|
|
360
|
+
/** Ireland: PPS Number — 7 digits + 1-2 letters */
|
|
361
|
+
function validateIE_PPSN(value) {
|
|
362
|
+
const cleaned = value.replace(/\s/g, '').toUpperCase();
|
|
363
|
+
if (!/^\d{7}[A-Z]{1,2}$/.test(cleaned)) {
|
|
364
|
+
return fail('PPS Number must be 7 digits followed by 1-2 letters');
|
|
365
|
+
}
|
|
366
|
+
const d = cleaned.slice(0, 7).split('').map(Number);
|
|
367
|
+
const weights = [8, 7, 6, 5, 4, 3, 2];
|
|
368
|
+
let sum = 0;
|
|
369
|
+
for (let i = 0; i < 7; i++)
|
|
370
|
+
sum += d[i] * weights[i];
|
|
371
|
+
// If there's a second letter (new format), add its value * 9
|
|
372
|
+
if (cleaned.length === 9) {
|
|
373
|
+
sum += (cleaned.charCodeAt(8) - 64) * 9;
|
|
374
|
+
}
|
|
375
|
+
const check = sum % 23;
|
|
376
|
+
const expected = check === 0 ? 'W' : String.fromCharCode(64 + check);
|
|
377
|
+
if (cleaned[7] !== expected) {
|
|
378
|
+
return fail('Invalid PPS Number check character');
|
|
379
|
+
}
|
|
380
|
+
return ok;
|
|
381
|
+
}
|
|
382
|
+
/** Lithuania: Asmens kodas — 11 digits */
|
|
383
|
+
function validateLT_AK(value) {
|
|
384
|
+
const cleaned = value.replace(/\s/g, '');
|
|
385
|
+
if (!/^\d{11}$/.test(cleaned)) {
|
|
386
|
+
return fail('Asmens kodas must be exactly 11 digits');
|
|
387
|
+
}
|
|
388
|
+
// Same algorithm as Estonia
|
|
389
|
+
const d = cleaned.split('').map(Number);
|
|
390
|
+
const weights1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 1];
|
|
391
|
+
let sum = 0;
|
|
392
|
+
for (let i = 0; i < 10; i++)
|
|
393
|
+
sum += d[i] * weights1[i];
|
|
394
|
+
let check = sum % 11;
|
|
395
|
+
if (check === 10) {
|
|
396
|
+
const weights2 = [3, 4, 5, 6, 7, 8, 9, 1, 2, 3];
|
|
397
|
+
sum = 0;
|
|
398
|
+
for (let i = 0; i < 10; i++)
|
|
399
|
+
sum += d[i] * weights2[i];
|
|
400
|
+
check = sum % 11;
|
|
401
|
+
if (check === 10)
|
|
402
|
+
check = 0;
|
|
403
|
+
}
|
|
404
|
+
if (check !== d[10]) {
|
|
405
|
+
return fail('Invalid Asmens kodas check digit');
|
|
406
|
+
}
|
|
407
|
+
return ok;
|
|
408
|
+
}
|
|
409
|
+
/** Portugal: NIF — 9 digits, mod 11 */
|
|
410
|
+
function validatePT_NIF(value) {
|
|
411
|
+
const cleaned = value.replace(/[\s-]/g, '');
|
|
412
|
+
if (!/^\d{9}$/.test(cleaned)) {
|
|
413
|
+
return fail('NIF must be exactly 9 digits');
|
|
414
|
+
}
|
|
415
|
+
const d = cleaned.split('').map(Number);
|
|
416
|
+
let sum = 0;
|
|
417
|
+
for (let i = 0; i < 8; i++) {
|
|
418
|
+
sum += d[i] * (9 - i);
|
|
419
|
+
}
|
|
420
|
+
const remainder = sum % 11;
|
|
421
|
+
const check = remainder < 2 ? 0 : 11 - remainder;
|
|
422
|
+
if (check !== d[8]) {
|
|
423
|
+
return fail('Invalid NIF check digit');
|
|
424
|
+
}
|
|
425
|
+
return ok;
|
|
426
|
+
}
|
|
427
|
+
/** Romania: CNP — 13 digits, weighted checksum */
|
|
428
|
+
function validateRO_CNP(value) {
|
|
429
|
+
const cleaned = value.replace(/\s/g, '');
|
|
430
|
+
if (!/^\d{13}$/.test(cleaned)) {
|
|
431
|
+
return fail('CNP must be exactly 13 digits');
|
|
432
|
+
}
|
|
433
|
+
const d = cleaned.split('').map(Number);
|
|
434
|
+
const weights = [2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9];
|
|
435
|
+
let sum = 0;
|
|
436
|
+
for (let i = 0; i < 12; i++) {
|
|
437
|
+
sum += d[i] * weights[i];
|
|
438
|
+
}
|
|
439
|
+
const remainder = sum % 11;
|
|
440
|
+
const check = remainder === 10 ? 1 : remainder;
|
|
441
|
+
if (check !== d[12]) {
|
|
442
|
+
return fail('Invalid CNP check digit');
|
|
443
|
+
}
|
|
444
|
+
return ok;
|
|
445
|
+
}
|
|
446
|
+
/** Slovakia: Rodné číslo — same rules as Czech Republic */
|
|
447
|
+
function validateSK_RC(value) {
|
|
448
|
+
return validateCZ_RC(value);
|
|
449
|
+
}
|
|
450
|
+
/** Slovenia: EMŠO — 13 digits, mod 11 weighted checksum */
|
|
451
|
+
function validateSI_EMSO(value) {
|
|
452
|
+
const cleaned = value.replace(/\s/g, '');
|
|
453
|
+
if (!/^\d{13}$/.test(cleaned)) {
|
|
454
|
+
return fail('EMŠO must be exactly 13 digits');
|
|
455
|
+
}
|
|
456
|
+
const d = cleaned.split('').map(Number);
|
|
457
|
+
const weights = [7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
|
|
458
|
+
let sum = 0;
|
|
459
|
+
for (let i = 0; i < 12; i++) {
|
|
460
|
+
sum += d[i] * weights[i];
|
|
461
|
+
}
|
|
462
|
+
const remainder = sum % 11;
|
|
463
|
+
const check = remainder === 0 ? 0 : 11 - remainder;
|
|
464
|
+
// Check digit of 10 is invalid
|
|
465
|
+
if (check === 10)
|
|
466
|
+
return fail('Invalid EMŠO (check digit would be 10)');
|
|
467
|
+
if (check !== d[12]) {
|
|
468
|
+
return fail('Invalid EMŠO check digit');
|
|
469
|
+
}
|
|
470
|
+
return ok;
|
|
471
|
+
}
|
|
472
|
+
// ─── Personal ID validator registry ─────────────────────────────────
|
|
473
|
+
exports.EU_PERSONAL_ID_VALIDATORS = {
|
|
474
|
+
'SE:PERSONNUMMER': validateSE_PERSONNUMMER,
|
|
475
|
+
'NL:BSN': validateNL_BSN,
|
|
476
|
+
'BE:RIJKSREGISTERNUMMER': validateBE_RRN,
|
|
477
|
+
'PL:PESEL': validatePL_PESEL,
|
|
478
|
+
'FI:HENKILOTUNNUS': validateFI_HETU,
|
|
479
|
+
'ES:DNI': validateES_DNI,
|
|
480
|
+
'ES:NIE': validateES_NIE,
|
|
481
|
+
'IT:CODICE_FISCALE': validateIT_CF,
|
|
482
|
+
'DE:STEUER_ID': validateDE_STEUERID,
|
|
483
|
+
'FR:NIR': validateFR_NIR,
|
|
484
|
+
'HR:OIB': validateHR_OIB,
|
|
485
|
+
'BG:EGN': validateBG_EGN,
|
|
486
|
+
'CZ:RODNE_CISLO': validateCZ_RC,
|
|
487
|
+
'DK:CPR': validateDK_CPR,
|
|
488
|
+
'EE:ISIKUKOOD': validateEE_IK,
|
|
489
|
+
'GR:AMKA': validateGR_AMKA,
|
|
490
|
+
'IE:PPSN': validateIE_PPSN,
|
|
491
|
+
'LT:ASMENS_KODAS': validateLT_AK,
|
|
492
|
+
'PT:NIF': validatePT_NIF,
|
|
493
|
+
'RO:CNP': validateRO_CNP,
|
|
494
|
+
'SK:RODNE_CISLO': validateSK_RC,
|
|
495
|
+
'SI:EMSO': validateSI_EMSO,
|
|
496
|
+
// Regex-only validation for countries without well-known checksum algorithms
|
|
497
|
+
'AT:ZMR': makeRegexValidator(/^\d{12}$/, '12 digits'),
|
|
498
|
+
'CY:ARC': makeRegexValidator(/^\d{1,10}$/, '1-10 digits'),
|
|
499
|
+
'HU:SZEMELYI_SZAM': makeRegexValidator(/^\d{6}[A-Z]{2}$/i, '6 digits + 2 letters'),
|
|
500
|
+
'HU:ADOAZONOSITO': (value) => {
|
|
501
|
+
const cleaned = value.replace(/[\s-]/g, '');
|
|
502
|
+
if (!/^\d{10}$/.test(cleaned))
|
|
503
|
+
return fail('Adóazonosító must be 10 digits');
|
|
504
|
+
return ok;
|
|
505
|
+
},
|
|
506
|
+
'LV:PERSONAS_KODS': makeRegexValidator(/^\d{6}-?\d{5}$/, 'DDMMYY-NNNNN'),
|
|
507
|
+
'LU:MATRICULE': makeRegexValidator(/^\d{13}$/, '13 digits'),
|
|
508
|
+
'MT:ID_CARD': makeRegexValidator(/^\d{1,7}[A-Z]$/i, '1-7 digits + letter'),
|
|
509
|
+
};
|
|
510
|
+
/** List of known personal ID types per country. */
|
|
511
|
+
exports.EU_PERSONAL_ID_TYPES = {
|
|
512
|
+
AT: ['ZMR'],
|
|
513
|
+
BE: ['RIJKSREGISTERNUMMER'],
|
|
514
|
+
BG: ['EGN'],
|
|
515
|
+
HR: ['OIB'],
|
|
516
|
+
CY: ['ARC'],
|
|
517
|
+
CZ: ['RODNE_CISLO'],
|
|
518
|
+
DK: ['CPR'],
|
|
519
|
+
EE: ['ISIKUKOOD'],
|
|
520
|
+
FI: ['HENKILOTUNNUS'],
|
|
521
|
+
FR: ['NIR'],
|
|
522
|
+
DE: ['STEUER_ID'],
|
|
523
|
+
GR: ['AMKA'],
|
|
524
|
+
HU: ['SZEMELYI_SZAM', 'ADOAZONOSITO'],
|
|
525
|
+
IE: ['PPSN'],
|
|
526
|
+
IT: ['CODICE_FISCALE'],
|
|
527
|
+
LV: ['PERSONAS_KODS'],
|
|
528
|
+
LT: ['ASMENS_KODAS'],
|
|
529
|
+
LU: ['MATRICULE'],
|
|
530
|
+
MT: ['ID_CARD'],
|
|
531
|
+
NL: ['BSN'],
|
|
532
|
+
PL: ['PESEL'],
|
|
533
|
+
PT: ['NIF'],
|
|
534
|
+
RO: ['CNP'],
|
|
535
|
+
SK: ['RODNE_CISLO'],
|
|
536
|
+
SI: ['EMSO'],
|
|
537
|
+
ES: ['DNI', 'NIE'],
|
|
538
|
+
SE: ['PERSONNUMMER'],
|
|
539
|
+
};
|
|
540
|
+
// ─── Company ID validators ──────────────────────────────────────────
|
|
541
|
+
/** VAT number format patterns per EU country */
|
|
542
|
+
const VAT_PATTERNS = {
|
|
543
|
+
AT: /^ATU\d{8}$/,
|
|
544
|
+
BE: /^BE[01]\d{9}$/,
|
|
545
|
+
BG: /^BG\d{9,10}$/,
|
|
546
|
+
HR: /^HR\d{11}$/,
|
|
547
|
+
CY: /^CY\d{8}[A-Z]$/,
|
|
548
|
+
CZ: /^CZ\d{8,10}$/,
|
|
549
|
+
DK: /^DK\d{8}$/,
|
|
550
|
+
EE: /^EE\d{9}$/,
|
|
551
|
+
FI: /^FI\d{8}$/,
|
|
552
|
+
FR: /^FR[0-9A-Z]{2}\d{9}$/,
|
|
553
|
+
DE: /^DE\d{9}$/,
|
|
554
|
+
GR: /^EL\d{9}$/,
|
|
555
|
+
HU: /^HU\d{8}$/,
|
|
556
|
+
IE: /^IE\d{7}[A-Z]{1,2}$/,
|
|
557
|
+
IT: /^IT\d{11}$/,
|
|
558
|
+
LV: /^LV\d{11}$/,
|
|
559
|
+
LT: /^LT(\d{9}|\d{12})$/,
|
|
560
|
+
LU: /^LU\d{8}$/,
|
|
561
|
+
MT: /^MT\d{8}$/,
|
|
562
|
+
NL: /^NL\d{9}B\d{2}$/,
|
|
563
|
+
PL: /^PL\d{10}$/,
|
|
564
|
+
PT: /^PT\d{9}$/,
|
|
565
|
+
RO: /^RO\d{2,10}$/,
|
|
566
|
+
SK: /^SK\d{10}$/,
|
|
567
|
+
SI: /^SI\d{8}$/,
|
|
568
|
+
ES: /^ES[A-Z0-9]\d{7}[A-Z0-9]$/,
|
|
569
|
+
SE: /^SE\d{12}$/,
|
|
570
|
+
};
|
|
571
|
+
/** Validate a VAT number. Value should include the country prefix (e.g. "SE556123456701"). */
|
|
572
|
+
function validateVAT(value) {
|
|
573
|
+
const cleaned = value.replace(/[\s.-]/g, '').toUpperCase();
|
|
574
|
+
if (cleaned.length < 4) {
|
|
575
|
+
return fail('VAT number must include country prefix (e.g. SE, DE, FR)');
|
|
576
|
+
}
|
|
577
|
+
// Extract country code (first 2 chars, except Greece uses EL)
|
|
578
|
+
const prefix = cleaned.slice(0, 2);
|
|
579
|
+
const countryCode = prefix === 'EL' ? 'GR' : prefix;
|
|
580
|
+
const pattern = VAT_PATTERNS[countryCode];
|
|
581
|
+
if (!pattern) {
|
|
582
|
+
return fail(`Unknown VAT country prefix: ${prefix}`);
|
|
583
|
+
}
|
|
584
|
+
if (!pattern.test(cleaned)) {
|
|
585
|
+
return fail(`Invalid VAT format for country ${prefix}`);
|
|
586
|
+
}
|
|
587
|
+
return ok;
|
|
588
|
+
}
|
|
589
|
+
/** Company registration number patterns per EU country */
|
|
590
|
+
const COMPANY_REG_PATTERNS = {
|
|
591
|
+
AT: /^FN\s?\d{5,6}[a-z]$/i, // Firmenbuchnummer
|
|
592
|
+
BE: /^0?\d{9,10}$/, // KBO/BCE number
|
|
593
|
+
BG: /^\d{9,13}$/, // EIK/BULSTAT
|
|
594
|
+
HR: /^\d{11}$/, // OIB (same as personal)
|
|
595
|
+
CY: /^HE\d{5,6}$/i, // Registration number
|
|
596
|
+
CZ: /^\d{8}$/, // IČO
|
|
597
|
+
DK: /^\d{8}$/, // CVR
|
|
598
|
+
EE: /^\d{8}$/, // Registry code
|
|
599
|
+
FI: /^\d{7}-?\d$/, // Y-tunnus
|
|
600
|
+
FR: /^\d{9}$/, // SIREN
|
|
601
|
+
DE: /^HR[AB]\s?\d{4,6}$/i, // Handelsregisternummer
|
|
602
|
+
GR: /^\d{12}$/, // GEMI
|
|
603
|
+
HU: /^\d{2}-\d{2}-\d{6}$/, // Cégjegyzékszám
|
|
604
|
+
IE: /^\d{5,6}$/, // CRO number
|
|
605
|
+
IT: /^\d{11}$/, // Partita IVA / REA
|
|
606
|
+
LV: /^\d{11}$/, // Registration number
|
|
607
|
+
LT: /^\d{7,9}$/, // JAR code
|
|
608
|
+
LU: /^[A-Z]\d{5,6}$/i, // RCS number
|
|
609
|
+
MT: /^C\s?\d{4,5}$/i, // Company number
|
|
610
|
+
NL: /^\d{8}$/, // KVK number
|
|
611
|
+
PL: /^\d{9,10}$/, // KRS or NIP
|
|
612
|
+
PT: /^\d{9}$/, // NIPC
|
|
613
|
+
RO: /^J\d{2}\/\d{1,6}\/\d{4}$/, // Registrul Comerțului
|
|
614
|
+
SK: /^\d{8}$/, // IČO
|
|
615
|
+
SI: /^\d{7,10}$/, // Matična številka
|
|
616
|
+
ES: /^[A-Z]\d{7}[A-Z0-9]$/i, // CIF
|
|
617
|
+
SE: /^\d{10}$/, // Organisationsnummer
|
|
618
|
+
};
|
|
619
|
+
/** Validate a company registration number for a given country. */
|
|
620
|
+
function validateCompanyReg(value, country) {
|
|
621
|
+
const cleaned = value.replace(/\s/g, '');
|
|
622
|
+
const pattern = COMPANY_REG_PATTERNS[country];
|
|
623
|
+
if (!pattern) {
|
|
624
|
+
return fail(`No company registration format known for country ${country}`);
|
|
625
|
+
}
|
|
626
|
+
if (!pattern.test(cleaned)) {
|
|
627
|
+
return fail(`Invalid company registration format for ${country}`);
|
|
628
|
+
}
|
|
629
|
+
// Additional checksum for Swedish organisationsnummer (Luhn on digit 1-9, similar to personnummer)
|
|
630
|
+
if (country === 'SE' && /^\d{10}$/.test(cleaned)) {
|
|
631
|
+
if (!luhn(cleaned)) {
|
|
632
|
+
return fail('Invalid Swedish organisationsnummer (Luhn check failed)');
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return ok;
|
|
636
|
+
}
|
|
637
|
+
/** Validate a Legal Entity Identifier (ISO 17442). 20 alphanumeric chars, mod 97-10. */
|
|
638
|
+
function validateLEI(value) {
|
|
639
|
+
const cleaned = value.replace(/\s/g, '').toUpperCase();
|
|
640
|
+
if (!/^[A-Z0-9]{20}$/.test(cleaned)) {
|
|
641
|
+
return fail('LEI must be exactly 20 alphanumeric characters');
|
|
642
|
+
}
|
|
643
|
+
if (!mod97_10(cleaned)) {
|
|
644
|
+
return fail('Invalid LEI check digits (mod 97-10)');
|
|
645
|
+
}
|
|
646
|
+
return ok;
|
|
647
|
+
}
|
|
648
|
+
/** Validate a DUNS number. 9 digits. */
|
|
649
|
+
function validateDUNS(value) {
|
|
650
|
+
const cleaned = value.replace(/[\s-]/g, '');
|
|
651
|
+
if (!/^\d{9}$/.test(cleaned)) {
|
|
652
|
+
return fail('DUNS number must be exactly 9 digits');
|
|
653
|
+
}
|
|
654
|
+
return ok;
|
|
655
|
+
}
|
|
656
|
+
/** Validate an EORI number. Country prefix + up to 15 alphanumeric chars. */
|
|
657
|
+
function validateEORI(value) {
|
|
658
|
+
const cleaned = value.replace(/\s/g, '').toUpperCase();
|
|
659
|
+
if (!/^[A-Z]{2}[A-Z0-9]{1,15}$/.test(cleaned)) {
|
|
660
|
+
return fail('EORI must be a 2-letter country prefix followed by 1-15 alphanumeric characters');
|
|
661
|
+
}
|
|
662
|
+
return ok;
|
|
663
|
+
}
|
|
664
|
+
//# sourceMappingURL=identity-validators.js.map
|