indycheck 0.0.0-stage → 0.2.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/index.js ADDED
@@ -0,0 +1,1311 @@
1
+ "use strict";
2
+ /**
3
+ * indycheck — offline Indian ID validator (pure-TypeScript mirror).
4
+ *
5
+ * This file mirrors the Rust core in crates/indycheck/src/ exactly:
6
+ * same validators, same order of checks, same stable `code` values, same
7
+ * `reason_en` / `reason_hi` strings, same `parsed` shapes (see
8
+ * shared/spec.md). Behaviour is defined by shared/test-vectors.json
9
+ * together with the Rust code; as of the last check the two agree on all
10
+ * 245 vectors (verified by emulating the Rust source over the file), so
11
+ * the vector test below asserts every entry directly with no exceptions.
12
+ * If the Rust side drifts again, that test fails loudly instead of
13
+ * silently picking a side.
14
+ *
15
+ * The 12 validators are: mobile, PAN, Aadhaar, GSTIN, IFSC, UPI, PIN
16
+ * code, TAN, CIN, LLPIN, Voter ID / EPIC and vehicle registration.
17
+ * TAN, CIN, LLPIN, Voter ID, vehicle and IFSC have no public checksum:
18
+ * only their format is checked (say so wherever these are documented).
19
+ * `validate*WithOptions` exists only where the Rust core has it — mobile,
20
+ * PAN, Aadhaar and GSTIN (options.rs); the other validators take no
21
+ * options in Rust either. `formatPan` / `formatGstin` return
22
+ * `string | null` and `maskPan` / `maskMobile` return a plain string
23
+ * ("" when the format is invalid), exactly like the Rust functions —
24
+ * unlike the legacy `formatMobile` / `maskAadhaar` wrappers, which keep
25
+ * returning a `ValidationResult` for backwards compatibility.
26
+ * The `generate*` functions produce mock test data only: syntactically
27
+ * valid, never a real issued ID — see generate.rs and the README.
28
+ *
29
+ * Notes on the Rust contract reproduced here:
30
+ * - Success code is `VALID` (not `OK`). The full code set is:
31
+ * VALID, EMPTY, INVALID_LENGTH, INVALID_FORMAT, INVALID_CHECKSUM,
32
+ * INVALID_FIRST_DIGIT, INVALID_STATE_CODE, INVALID_ENTITY,
33
+ * INVALID_HANDLE, UNKNOWN_TYPE. `INVALID_TYPE` (non-string input) and
34
+ * `SOME_INVALID` (validateMany batch result) exist only in the JS/Python
35
+ * wrappers — Rust takes `&str` and returns a bare `Vec` for batches.
36
+ * - `reason_hi` is Roman-script Hinglish in the Rust core (e.g.
37
+ * "PAN khaali hai"), not Devanagari — reproduced verbatim, even though
38
+ * spec.md calls it Devanagari Hindi.
39
+ * - PAN entity checking is lenient by design (pan.rs, spec.md): the 4th
40
+ * character is reported in `parsed` (entity name, or "Unknown" when it
41
+ * is not in the published list) and the PAN is still format-valid.
42
+ * The canonical example ABCDE1234F is valid with entity "Unknown"
43
+ * (4th char D). `INVALID_ENTITY` is a reserved stable code, not
44
+ * emitted in v0.1.
45
+ * - `detect` returns the Rust detect string ("pan" | "aadhaar" | "gstin" |
46
+ * "ifsc" | "upi" | "mobile" | "pincode" | "tan" | "cin" | "llpin" |
47
+ * "voter" | "vehicle" | "unknown") in `parsed.type`.
48
+ * It is shape-based, not validity-based: detection can succeed while
49
+ * validation of the same input fails. Its order is fixed by lib.rs:
50
+ * `@` -> upi first; then a Verhoeff-valid 12-digit number -> aadhaar
51
+ * (including spaced / hyphenated Aadhaar, digits-only); then any
52
+ * normalisable mobile -> mobile; then CIN (21 chars); then by length
53
+ * (15 -> gstin, 12 digits -> aadhaar, 11 IFSC-pattern -> ifsc,
54
+ * 10 PAN-pattern -> pan, 10 TAN-pattern -> tan, 10 voter-pattern ->
55
+ * voter, 10 digits -> mobile, 7 LLPIN-pattern -> llpin, 6 digits ->
56
+ * pincode); then formatted mobiles, then PAN / TAN / voter / IFSC /
57
+ * LLPIN patterns, then vehicle. Note the code's order, not spec.md's
58
+ * one-line summary of it.
59
+ * - `formatMobile` returns the validateMobile result (parsed.normalized
60
+ * is the 10-digit string, Rust's `format_mobile` return). `maskAadhaar`
61
+ * returns the validateAadhaar result (parsed.masked). Unlike the Rust
62
+ * `mask_aadhaar` string function — which masks any 12-digit string,
63
+ * even one with a bad checksum — the JS/Python maskAadhaar only
64
+ * succeeds for a fully valid Aadhaar number.
65
+ * - UPI handle list, GSTIN state names (incl. 25/26) and PAN entity
66
+ * names are copied verbatim from crates/indycheck/src/data.rs and
67
+ * pan.rs (note: B -> "BODY", H -> "HUF", A -> "AOP").
68
+ *
69
+ * Honest limits: format + checksum only. Nothing here checks a number
70
+ * against UIDAI, NSDL / Income Tax, GSTN, a bank, or any other live
71
+ * database. A `valid: true` result means "well-formed and checksum-correct",
72
+ * not "this ID really exists / belongs to this person".
73
+ *
74
+ * Every validator returns `{ valid, code, reason_en, reason_hi, parsed }`
75
+ * and never throws on bad input.
76
+ */
77
+ Object.defineProperty(exports, "__esModule", { value: true });
78
+ exports.DEFAULT_OPTIONS = exports.UPI_HANDLES = exports.GSTIN_STATE_NAMES = exports.PAN_ENTITY_TYPES = void 0;
79
+ exports.defaultOptions = defaultOptions;
80
+ exports.lenientOptions = lenientOptions;
81
+ exports.noSpacesHyphensOptions = noSpacesHyphensOptions;
82
+ exports.validateMobile = validateMobile;
83
+ exports.validateMobileWithOptions = validateMobileWithOptions;
84
+ exports.formatMobile = formatMobile;
85
+ exports.formatMobileWithOptions = formatMobileWithOptions;
86
+ exports.maskMobile = maskMobile;
87
+ exports.validatePan = validatePan;
88
+ exports.validatePanWithOptions = validatePanWithOptions;
89
+ exports.formatPan = formatPan;
90
+ exports.maskPan = maskPan;
91
+ exports.maskAadhaarString = maskAadhaarString;
92
+ exports.validateAadhaar = validateAadhaar;
93
+ exports.validateAadhaarWithOptions = validateAadhaarWithOptions;
94
+ exports.maskAadhaar = maskAadhaar;
95
+ exports.validateGstin = validateGstin;
96
+ exports.validateGstinWithOptions = validateGstinWithOptions;
97
+ exports.formatGstin = formatGstin;
98
+ exports.validateIfsc = validateIfsc;
99
+ exports.validateUpi = validateUpi;
100
+ exports.validatePincode = validatePincode;
101
+ exports.validateTan = validateTan;
102
+ exports.validateCin = validateCin;
103
+ exports.validateLlpin = validateLlpin;
104
+ exports.validateVoterId = validateVoterId;
105
+ exports.validateEpic = validateEpic;
106
+ exports.validateVehicle = validateVehicle;
107
+ exports.validateVehicleRegistration = validateVehicleRegistration;
108
+ exports.generatePan = generatePan;
109
+ exports.generateAadhaar = generateAadhaar;
110
+ exports.generateGstin = generateGstin;
111
+ exports.generateMobile = generateMobile;
112
+ exports.detectType = detectType;
113
+ exports.detect = detect;
114
+ exports.validateMany = validateMany;
115
+ // ---------------------------------------------------------------------------
116
+ // Shared tables — verbatim from crates/indycheck/src/data.rs and pan.rs
117
+ // ---------------------------------------------------------------------------
118
+ exports.PAN_ENTITY_TYPES = {
119
+ A: "AOP",
120
+ B: "BODY",
121
+ C: "Company",
122
+ F: "Firm",
123
+ G: "Government",
124
+ H: "HUF",
125
+ J: "Artificial Juridical Person",
126
+ L: "Local Authority",
127
+ P: "Individual",
128
+ T: "Trust",
129
+ };
130
+ exports.GSTIN_STATE_NAMES = {
131
+ "01": "Jammu and Kashmir",
132
+ "02": "Himachal Pradesh",
133
+ "03": "Punjab",
134
+ "04": "Chandigarh",
135
+ "05": "Uttarakhand",
136
+ "06": "Haryana",
137
+ "07": "Delhi",
138
+ "08": "Rajasthan",
139
+ "09": "Uttar Pradesh",
140
+ "10": "Bihar",
141
+ "11": "Sikkim",
142
+ "12": "Arunachal Pradesh",
143
+ "13": "Nagaland",
144
+ "14": "Manipur",
145
+ "15": "Mizoram",
146
+ "16": "Tripura",
147
+ "17": "Meghalaya",
148
+ "18": "Assam",
149
+ "19": "West Bengal",
150
+ "20": "Jharkhand",
151
+ "21": "Odisha",
152
+ "22": "Chhattisgarh",
153
+ "23": "Madhya Pradesh",
154
+ "24": "Gujarat",
155
+ "25": "Daman and Diu and Dadra and Nagar Haveli",
156
+ "26": "Dadra and Nagar Haveli and Daman and Diu",
157
+ "27": "Maharashtra",
158
+ "28": "Andhra Pradesh",
159
+ "29": "Karnataka",
160
+ "30": "Goa",
161
+ "31": "Lakshadweep",
162
+ "32": "Kerala",
163
+ "33": "Tamil Nadu",
164
+ "34": "Puducherry",
165
+ "35": "Andaman and Nicobar Islands",
166
+ "36": "Telangana",
167
+ "37": "Andhra Pradesh",
168
+ "38": "Ladakh",
169
+ };
170
+ /** Common UPI handles, verbatim from crates/indycheck/src/data.rs. */
171
+ exports.UPI_HANDLES = [
172
+ "paytm",
173
+ "okaxis",
174
+ "okhdfcbank",
175
+ "okicici",
176
+ "oksbi",
177
+ "okbizaxis",
178
+ "ybl",
179
+ "axl",
180
+ "ibl",
181
+ "upi",
182
+ "jio",
183
+ "aubank",
184
+ "freecharge",
185
+ "amazonpay",
186
+ "okbob",
187
+ "okpnb",
188
+ "okciti",
189
+ "barodampay",
190
+ "cnrb",
191
+ "idfcfirst",
192
+ "kvb",
193
+ "rbl",
194
+ "sbi",
195
+ "hdfc",
196
+ "icici",
197
+ "axis",
198
+ "kotak",
199
+ "yesbank",
200
+ "indus",
201
+ "federal",
202
+ "okyes",
203
+ "payzapp",
204
+ "slice",
205
+ "gpay",
206
+ "phonepe",
207
+ "bhim",
208
+ ];
209
+ // ---------------------------------------------------------------------------
210
+ // Result helpers (never throw)
211
+ // ---------------------------------------------------------------------------
212
+ function ok(reason_en, reason_hi, parsed) {
213
+ return { valid: true, code: "VALID", reason_en, reason_hi, parsed };
214
+ }
215
+ function fail(code, reason_en, reason_hi, parsed = null) {
216
+ return { valid: false, code, reason_en, reason_hi, parsed };
217
+ }
218
+ /** JS/Python-only code: the Rust core takes &str, so it has no equivalent. */
219
+ function invalidType() {
220
+ return fail("INVALID_TYPE", "Input must be a string.", "Input string hona chahiye.");
221
+ }
222
+ function empty(reason_en, reason_hi) {
223
+ return fail("EMPTY", reason_en, reason_hi);
224
+ }
225
+ /** ASCII-only upper-casing (Rust's to_ascii_uppercase; avoids e.g. ß -> SS). */
226
+ function asciiUpper(s) {
227
+ return s.replace(/[a-z]/g, (c) => c.toUpperCase());
228
+ }
229
+ function charsOf(s) {
230
+ return [...s];
231
+ }
232
+ function isAsciiAlpha(c) {
233
+ return (c >= "A" && c <= "Z") || (c >= "a" && c <= "z");
234
+ }
235
+ function isAsciiDigit(c) {
236
+ return c >= "0" && c <= "9";
237
+ }
238
+ function isAsciiAlnum(c) {
239
+ return isAsciiAlpha(c) || isAsciiDigit(c);
240
+ }
241
+ /** Coerce an unknown input to a trimmed string, or return null + the reason to use. */
242
+ function asString(input) {
243
+ if (typeof input !== "string")
244
+ return { value: null, error: invalidType() };
245
+ const value = input.trim();
246
+ if (value === "")
247
+ return { value: null, error: null };
248
+ return { value, error: null };
249
+ }
250
+ // ---------------------------------------------------------------------------
251
+ // Options — verbatim from options.rs
252
+ // ---------------------------------------------------------------------------
253
+ exports.DEFAULT_OPTIONS = {
254
+ strict: true,
255
+ allow_spaces_hyphens: true,
256
+ normalise_plus91: true,
257
+ uppercase: true,
258
+ };
259
+ /** Strict validation (the default): every rule enforced. */
260
+ function defaultOptions() {
261
+ return { ...exports.DEFAULT_OPTIONS };
262
+ }
263
+ /** Lenient validation: format only — first-digit and checksum checks
264
+ * are skipped. */
265
+ function lenientOptions() {
266
+ return { ...exports.DEFAULT_OPTIONS, strict: false };
267
+ }
268
+ /** Start from the defaults and turn off space/hyphen tolerance. */
269
+ function noSpacesHyphensOptions() {
270
+ return { ...exports.DEFAULT_OPTIONS, allow_spaces_hyphens: false };
271
+ }
272
+ /** Merge a partial options object onto the defaults. */
273
+ function resolveOptions(options) {
274
+ return { ...exports.DEFAULT_OPTIONS, ...(options ?? {}) };
275
+ }
276
+ /** Rust `spaces_hyphens_ok`: rejects spaces/hyphens/tabs in the raw
277
+ * input when the option forbids them. Note this checks the raw input,
278
+ * so leading/trailing whitespace counts too. */
279
+ function spacesHyphensOk(input, options) {
280
+ return (options.allow_spaces_hyphens ||
281
+ (!input.includes(" ") && !input.includes("-") && !input.includes("\t")));
282
+ }
283
+ /** Rust `maybe_uppercase`: trims, then upper-cases when the option says so. */
284
+ function maybeUppercase(input, options) {
285
+ const trimmed = input.trim();
286
+ return options.uppercase ? asciiUpper(trimmed) : trimmed;
287
+ }
288
+ /** Rust `strip_spaces_hyphens`: strips whitespace and hyphens when the
289
+ * option allows it, otherwise returns the input unchanged. */
290
+ function stripSpacesHyphens(input, options) {
291
+ if (!options.allow_spaces_hyphens)
292
+ return input;
293
+ return [...input].filter((c) => !/\s/.test(c) && c !== "-").join("");
294
+ }
295
+ // ---------------------------------------------------------------------------
296
+ // Verhoeff (Aadhaar checksum) — tables verbatim from verhoeff.rs
297
+ // ---------------------------------------------------------------------------
298
+ const VERHOEFF_D = [
299
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
300
+ [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
301
+ [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
302
+ [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
303
+ [4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
304
+ [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
305
+ [6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
306
+ [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
307
+ [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
308
+ [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
309
+ ];
310
+ const VERHOEFF_P = [
311
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
312
+ [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
313
+ [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
314
+ [8, 0, 1, 3, 2, 4, 9, 6, 7, 5],
315
+ [9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
316
+ [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
317
+ [2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
318
+ [7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
319
+ ];
320
+ function verhoeffValid(digits) {
321
+ let c = 0;
322
+ const reversed = charsOf(digits).reverse();
323
+ for (let i = 0; i < reversed.length; i++) {
324
+ c = VERHOEFF_D[c][VERHOEFF_P[i % 8][Number(reversed[i])]];
325
+ }
326
+ return c === 0;
327
+ }
328
+ const VERHOEFF_INV = [0, 4, 3, 2, 1, 5, 6, 7, 8, 9];
329
+ /** Rust `verhoeff::verify_digit`: the check digit for a digit base
330
+ * (the check digit occupies position 0, so base digits start at 1). */
331
+ function verhoeffVerifyDigit(base) {
332
+ let c = 0;
333
+ const reversed = charsOf(base).reverse();
334
+ for (let i = 0; i < reversed.length; i++) {
335
+ c = VERHOEFF_D[c][VERHOEFF_P[(i + 1) % 8][Number(reversed[i])]];
336
+ }
337
+ return String(VERHOEFF_INV[c]);
338
+ }
339
+ // ---------------------------------------------------------------------------
340
+ // GSTIN Mod-36 Luhn (alphabet 0-9A-Z) — verbatim from luhn36.rs
341
+ // ---------------------------------------------------------------------------
342
+ const GSTIN_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
343
+ function gstinCheckDigit(first14) {
344
+ if (charsOf(first14).length !== 14)
345
+ return null;
346
+ let factor = 2;
347
+ let sum = 0;
348
+ for (const c of charsOf(first14).reverse()) {
349
+ const code = GSTIN_ALPHABET.indexOf(c);
350
+ if (code < 0)
351
+ return null;
352
+ const product = code * factor;
353
+ sum += Math.floor(product / 36) + (product % 36);
354
+ factor = factor === 2 ? 1 : 2;
355
+ }
356
+ const check = (36 - (sum % 36)) % 36;
357
+ return GSTIN_ALPHABET[check];
358
+ }
359
+ function gstinCheckDigitValid(gstin) {
360
+ const chars = charsOf(gstin);
361
+ if (chars.length !== 15)
362
+ return false;
363
+ const expected = gstinCheckDigit(chars.slice(0, 14).join(""));
364
+ return expected !== null && expected === chars[14];
365
+ }
366
+ // ---------------------------------------------------------------------------
367
+ // Mobile — verbatim from mobile.rs
368
+ // ---------------------------------------------------------------------------
369
+ /** Rust `normalize_mobile`: strips whitespace + hyphens, one leading '+',
370
+ * then a 91 / 0 prefix only when the rest is exactly 10 digits. */
371
+ function normalizeMobile(input) {
372
+ return normalizeMobileWithOptions(input, exports.DEFAULT_OPTIONS);
373
+ }
374
+ /** Same as `normalizeMobile`, with caller-controlled `Options`.
375
+ * With `normalise_plus91: false` only a bare 10-digit number is accepted;
376
+ * with `allow_spaces_hyphens: false` any space or hyphen rejects the input. */
377
+ function normalizeMobileWithOptions(input, options) {
378
+ let cleaned;
379
+ if (options.allow_spaces_hyphens) {
380
+ cleaned = input
381
+ .trim()
382
+ .split("")
383
+ .filter((c) => !/\s/.test(c) && c !== "-")
384
+ .join("");
385
+ }
386
+ else {
387
+ if ([...input].some((c) => /\s/.test(c) || c === "-"))
388
+ return null;
389
+ cleaned = input.trim();
390
+ }
391
+ if (!options.normalise_plus91) {
392
+ if (cleaned.length === 10 && charsOf(cleaned).every(isAsciiDigit))
393
+ return cleaned;
394
+ return null;
395
+ }
396
+ const withoutPlus = cleaned.startsWith("+") ? cleaned.slice(1) : cleaned;
397
+ let digits = withoutPlus;
398
+ if (withoutPlus.startsWith("91") && withoutPlus.slice(2).length === 10) {
399
+ digits = withoutPlus.slice(2);
400
+ }
401
+ else if (withoutPlus.startsWith("0") && withoutPlus.slice(1).length === 10) {
402
+ digits = withoutPlus.slice(1);
403
+ }
404
+ if (digits.length === 10 && charsOf(digits).every(isAsciiDigit))
405
+ return digits;
406
+ return null;
407
+ }
408
+ /** Rust `format_mobile_with_options` as a plain string: the normalised
409
+ * 10-digit number, or `null`. Lenient options skip the first-digit
410
+ * (6-9) rule. */
411
+ function formatMobileString(input, options) {
412
+ const normalized = normalizeMobileWithOptions(input, options);
413
+ if (normalized === null)
414
+ return null;
415
+ if (!options.strict || /^[6-9]/.test(normalized))
416
+ return normalized;
417
+ return null;
418
+ }
419
+ function validateMobile(input) {
420
+ return validateMobileWithOptions(input);
421
+ }
422
+ /** Same as `validateMobile`, with caller-controlled `Options`.
423
+ * With `strict: false` the first-digit rule is skipped (format only). */
424
+ function validateMobileWithOptions(input, options) {
425
+ try {
426
+ if (typeof input !== "string")
427
+ return invalidType();
428
+ const opts = resolveOptions(options);
429
+ if (input.trim() === "") {
430
+ return empty("Mobile number is empty", "Mobile number khaali hai");
431
+ }
432
+ const normalized = normalizeMobileWithOptions(input, opts);
433
+ if (normalized === null) {
434
+ return fail("INVALID_FORMAT", "Mobile number must be 10 digits, optionally with +91, 91 or 0 prefix", "Mobile number 10 digit ka hona chahiye, +91, 91 ya 0 ke saath bhi chalega");
435
+ }
436
+ if (opts.strict && !/^[6-9]/.test(normalized)) {
437
+ return fail("INVALID_FIRST_DIGIT", "Indian mobile numbers start with 6, 7, 8 or 9", "Bharat ke mobile number 6, 7, 8 ya 9 se shuru hote hain");
438
+ }
439
+ return ok("Mobile number format is valid", "Mobile number ka format sahi hai", {
440
+ normalized,
441
+ });
442
+ }
443
+ catch {
444
+ return invalidType();
445
+ }
446
+ }
447
+ function formatMobile(input) {
448
+ // Same contract as validateMobile; `parsed.normalized` is the 10-digit
449
+ // string (Rust `format_mobile` returns exactly that, or None).
450
+ return validateMobile(input);
451
+ }
452
+ function formatMobileWithOptions(input, options) {
453
+ return validateMobileWithOptions(input, options);
454
+ }
455
+ /** Masks a mobile number as `******3210` (6 asterisks + last 4 digits).
456
+ * Returns an empty string when the input is not a valid-format mobile
457
+ * number. A non-empty result is not proof of validity. */
458
+ function maskMobile(input) {
459
+ if (typeof input !== "string")
460
+ return "";
461
+ const normalized = formatMobileString(input, exports.DEFAULT_OPTIONS);
462
+ return normalized === null ? "" : `******${normalized.slice(6)}`;
463
+ }
464
+ // ---------------------------------------------------------------------------
465
+ // PAN — verbatim from pan.rs
466
+ // ---------------------------------------------------------------------------
467
+ function validatePan(input) {
468
+ return validatePanWithOptions(input);
469
+ }
470
+ /** Same as `validatePan`, with caller-controlled `Options`.
471
+ * For PAN, `strict`/`lenient` make no difference (there is no checksum or
472
+ * first-digit rule to skip); `uppercase` and `allow_spaces_hyphens` do. */
473
+ function validatePanWithOptions(input, options) {
474
+ try {
475
+ if (typeof input !== "string")
476
+ return invalidType();
477
+ const opts = resolveOptions(options);
478
+ if (input.trim() === "") {
479
+ return empty("PAN is empty", "PAN khaali hai");
480
+ }
481
+ if (!spacesHyphensOk(input, opts)) {
482
+ return fail("INVALID_FORMAT", "PAN must not contain spaces or hyphens with these options", "In options ke saath PAN me space ya hyphen nahi hona chahiye");
483
+ }
484
+ const upper = stripSpacesHyphens(maybeUppercase(input, opts), opts);
485
+ if (!opts.uppercase && charsOf(upper).some((c) => c >= "a" && c <= "z")) {
486
+ return fail("INVALID_FORMAT", "PAN must be upper-case with these options", "In options ke saath PAN upper-case me hona chahiye");
487
+ }
488
+ if (charsOf(upper).length !== 10) {
489
+ return fail("INVALID_LENGTH", "PAN must be exactly 10 characters", "PAN bilkul 10 character ka hona chahiye");
490
+ }
491
+ const chars = charsOf(upper);
492
+ const formatOk = chars.slice(0, 5).every(isAsciiAlpha) &&
493
+ chars.slice(5, 9).every(isAsciiDigit) &&
494
+ isAsciiAlpha(chars[9]);
495
+ if (!formatOk) {
496
+ return fail("INVALID_FORMAT", "PAN format must be 5 letters, 4 digits, 1 letter (e.g. ABCDE1234F). PAN has no public checksum, so only the format is checked", "PAN ka format 5 akshar, 4 digit, 1 akshar hona chahiye (jaise ABCDE1234F). PAN ka koi public checksum nahi hai, isliye sirf format check hota hai");
497
+ }
498
+ // An unlisted 4th character is reported as entity "Unknown" and the
499
+ // PAN is still format-valid (pan.rs). INVALID_ENTITY is a reserved
500
+ // stable code, not emitted in v0.1.
501
+ const entity = exports.PAN_ENTITY_TYPES[chars[3]] ?? "Unknown";
502
+ return ok("PAN format is valid. PAN has no public checksum, so this only checks the format", "PAN ka format sahi hai. PAN ka koi public checksum nahi hai, isliye sirf format check hua hai", { entity, entity_code: chars[3] });
503
+ }
504
+ catch {
505
+ return invalidType();
506
+ }
507
+ }
508
+ /** Formats a PAN: upper-cases it and strips spaces/hyphens.
509
+ * Returns `"ABCDE1234F"` when the result matches the PAN pattern,
510
+ * otherwise `null`. */
511
+ function formatPan(input) {
512
+ if (typeof input !== "string")
513
+ return null;
514
+ try {
515
+ const upper = stripSpacesHyphens(maybeUppercase(input, exports.DEFAULT_OPTIONS), exports.DEFAULT_OPTIONS);
516
+ return isPanPattern(upper) ? upper : null;
517
+ }
518
+ catch {
519
+ return null;
520
+ }
521
+ }
522
+ /** Masks a PAN as `ABCDE****F` (first 5 chars, 4 asterisks, last char).
523
+ * Returns an empty string when the input is not a valid-format PAN.
524
+ * A non-empty result is not proof of validity — use `validatePan`. */
525
+ function maskPan(input) {
526
+ const pan = formatPan(input);
527
+ return pan === null ? "" : `${pan.slice(0, 5)}****${pan.slice(9)}`;
528
+ }
529
+ // ---------------------------------------------------------------------------
530
+ // Aadhaar — verbatim from aadhaar.rs
531
+ // ---------------------------------------------------------------------------
532
+ function maskAadhaarString(input) {
533
+ const cleaned = input
534
+ .trim()
535
+ .split("")
536
+ .filter((c) => !/\s/.test(c) && c !== "-")
537
+ .join("");
538
+ if (charsOf(cleaned).length !== 12 || !charsOf(cleaned).every(isAsciiDigit))
539
+ return "";
540
+ return `XXXX XXXX ${cleaned.slice(8)}`;
541
+ }
542
+ function validateAadhaar(input) {
543
+ return validateAadhaarWithOptions(input);
544
+ }
545
+ /** Same as `validateAadhaar`, with caller-controlled `Options`.
546
+ * With `strict: false` the first-digit and Verhoeff checksum checks are
547
+ * skipped (12 digits only). With `allow_spaces_hyphens: false`, spaces
548
+ * or hyphens in the input are an `INVALID_FORMAT` error. */
549
+ function validateAadhaarWithOptions(input, options) {
550
+ try {
551
+ if (typeof input !== "string")
552
+ return invalidType();
553
+ const opts = resolveOptions(options);
554
+ if (input.trim() === "") {
555
+ return empty("Aadhaar number is empty", "Aadhaar number khaali hai");
556
+ }
557
+ if (!spacesHyphensOk(input, opts)) {
558
+ return fail("INVALID_FORMAT", "Aadhaar number must not contain spaces or hyphens with these options", "In options ke saath Aadhaar number me space ya hyphen nahi hona chahiye");
559
+ }
560
+ const cleaned = stripSpacesHyphens(input.trim(), opts);
561
+ if (charsOf(cleaned).length !== 12) {
562
+ return fail("INVALID_LENGTH", "Aadhaar number must be exactly 12 digits", "Aadhaar number bilkul 12 digit ka hona chahiye");
563
+ }
564
+ if (!charsOf(cleaned).every(isAsciiDigit)) {
565
+ return fail("INVALID_FORMAT", "Aadhaar number must contain only digits", "Aadhaar number me sirf digit hone chahiye");
566
+ }
567
+ if (opts.strict && !/^[2-9]/.test(cleaned)) {
568
+ return fail("INVALID_FIRST_DIGIT", "Aadhaar number must start with 2-9", "Aadhaar number 2 se 9 ke beech wale digit se shuru hona chahiye");
569
+ }
570
+ if (opts.strict && !verhoeffValid(cleaned)) {
571
+ return fail("INVALID_CHECKSUM", "Aadhaar checksum is invalid (Verhoeff check failed)", "Aadhaar ka checksum galat hai (Verhoeff check fail hua)");
572
+ }
573
+ return ok("Aadhaar number format and checksum are valid", "Aadhaar number ka format aur checksum sahi hai", { masked: maskAadhaarString(cleaned) });
574
+ }
575
+ catch {
576
+ return invalidType();
577
+ }
578
+ }
579
+ function maskAadhaar(input) {
580
+ // Same contract as validateAadhaar; `parsed.masked` is the result.
581
+ // (Differs from Rust `mask_aadhaar`, which masks any 12-digit string.)
582
+ return validateAadhaar(input);
583
+ }
584
+ // ---------------------------------------------------------------------------
585
+ // GSTIN — verbatim from gstin.rs
586
+ // ---------------------------------------------------------------------------
587
+ function validateGstin(input) {
588
+ return validateGstinWithOptions(input);
589
+ }
590
+ /** Same as `validateGstin`, with caller-controlled `Options`.
591
+ * With `strict: false` the Mod-36 check-digit check is skipped (format
592
+ * and state code still checked). With `allow_spaces_hyphens: false`,
593
+ * spaces or hyphens in the input are an `INVALID_FORMAT` error. */
594
+ function validateGstinWithOptions(input, options) {
595
+ try {
596
+ if (typeof input !== "string")
597
+ return invalidType();
598
+ const opts = resolveOptions(options);
599
+ if (input.trim() === "") {
600
+ return empty("GSTIN is empty", "GSTIN khaali hai");
601
+ }
602
+ if (!spacesHyphensOk(input, opts)) {
603
+ return fail("INVALID_FORMAT", "GSTIN must not contain spaces or hyphens with these options", "In options ke saath GSTIN me space ya hyphen nahi hona chahiye");
604
+ }
605
+ const upper = stripSpacesHyphens(maybeUppercase(input, opts), opts);
606
+ if (!opts.uppercase && charsOf(upper).some((c) => c >= "a" && c <= "z")) {
607
+ return fail("INVALID_FORMAT", "GSTIN must be upper-case with these options", "In options ke saath GSTIN upper-case me hona chahiye");
608
+ }
609
+ if (charsOf(upper).length !== 15) {
610
+ return fail("INVALID_LENGTH", "GSTIN must be exactly 15 characters", "GSTIN bilkul 15 character ka hona chahiye");
611
+ }
612
+ const chars = charsOf(upper);
613
+ if (!chars.slice(0, 2).every(isAsciiDigit)) {
614
+ return fail("INVALID_FORMAT", "GSTIN must start with a 2-digit state code", "GSTIN 2 digit ke state code se shuru hona chahiye");
615
+ }
616
+ const stateCode = chars.slice(0, 2).join("");
617
+ const state = exports.GSTIN_STATE_NAMES[stateCode];
618
+ if (!state) {
619
+ return fail("INVALID_STATE_CODE", "GSTIN state code is not a valid Indian state code (01-38)", "GSTIN ka state code sahi nahi hai (01-38 ke beech hona chahiye)");
620
+ }
621
+ const pan = chars.slice(2, 12).join("");
622
+ const panChars = charsOf(pan);
623
+ const panFormatOk = panChars.slice(0, 5).every(isAsciiAlpha) &&
624
+ panChars.slice(5, 9).every(isAsciiDigit) &&
625
+ isAsciiAlpha(panChars[9]);
626
+ if (!panFormatOk) {
627
+ return fail("INVALID_FORMAT", "GSTIN characters 3-12 must be a PAN in 5 letters, 4 digits, 1 letter format", "GSTIN ke 3 se 12 tak ke character PAN format me hone chahiye (5 akshar, 4 digit, 1 akshar)");
628
+ }
629
+ if (!isAsciiAlnum(chars[12])) {
630
+ return fail("INVALID_FORMAT", "GSTIN 13th character must be a letter or digit (entity number)", "GSTIN ka 13th character akshar ya digit hona chahiye");
631
+ }
632
+ if (chars[13] !== "Z") {
633
+ return fail("INVALID_FORMAT", "GSTIN 14th character must be Z", "GSTIN ka 14th character Z hona chahiye");
634
+ }
635
+ if (!isAsciiAlnum(chars[14])) {
636
+ return fail("INVALID_FORMAT", "GSTIN 15th character must be a letter or digit (check digit)", "GSTIN ka 15th character akshar ya digit hona chahiye");
637
+ }
638
+ if (opts.strict && !gstinCheckDigitValid(upper)) {
639
+ return fail("INVALID_CHECKSUM", "GSTIN check digit is invalid (Mod-36 Luhn check failed)", "GSTIN ka check digit galat hai (Mod-36 Luhn check fail hua)");
640
+ }
641
+ return ok("GSTIN format, state code and check digit are valid", "GSTIN ka format, state code aur check digit sahi hai", {
642
+ state,
643
+ state_code: stateCode,
644
+ pan,
645
+ });
646
+ }
647
+ catch {
648
+ return invalidType();
649
+ }
650
+ }
651
+ /** Formats a GSTIN: upper-cases it and strips spaces/hyphens.
652
+ * Returns the formatted GSTIN when its structure is in place (state
653
+ * code, PAN part and `Z`); the check digit is NOT verified here —
654
+ * so a `formatGstin` result can still fail `validateGstin`.
655
+ * Returns `null` otherwise. */
656
+ function formatGstin(input) {
657
+ if (typeof input !== "string")
658
+ return null;
659
+ try {
660
+ const result = validateGstinWithOptions(input, lenientOptions());
661
+ if (!result.valid)
662
+ return null;
663
+ return stripSpacesHyphens(maybeUppercase(input, exports.DEFAULT_OPTIONS), exports.DEFAULT_OPTIONS);
664
+ }
665
+ catch {
666
+ return null;
667
+ }
668
+ }
669
+ // ---------------------------------------------------------------------------
670
+ // IFSC — verbatim from ifsc.rs
671
+ // ---------------------------------------------------------------------------
672
+ function validateIfsc(input) {
673
+ try {
674
+ const { value, error } = asString(input);
675
+ if (error)
676
+ return error;
677
+ if (value === null) {
678
+ return empty("IFSC is empty", "IFSC khaali hai");
679
+ }
680
+ const upper = asciiUpper(value);
681
+ if (charsOf(upper).length !== 11) {
682
+ return fail("INVALID_LENGTH", "IFSC must be exactly 11 characters", "IFSC bilkul 11 character ka hona chahiye");
683
+ }
684
+ const chars = charsOf(upper);
685
+ const formatOk = chars.slice(0, 4).every(isAsciiAlpha) &&
686
+ chars[4] === "0" &&
687
+ chars.slice(5).every(isAsciiAlnum);
688
+ if (!formatOk) {
689
+ return fail("INVALID_FORMAT", "IFSC format must be 4 letters, 0, then 6 letters/digits (e.g. SBIN0000123). IFSC has no checksum, so only the format is checked", "IFSC ka format 4 akshar, 0, phir 6 akshar/digit hona chahiye (jaise SBIN0000123). IFSC ka koi checksum nahi hai, isliye sirf format check hota hai");
690
+ }
691
+ return ok("IFSC format is valid. IFSC has no checksum, so this only checks the format", "IFSC ka format sahi hai. IFSC ka koi checksum nahi hai, isliye sirf format check hua hai", { bank_code: chars.slice(0, 4).join(""), branch_code: chars.slice(5).join("") });
692
+ }
693
+ catch {
694
+ return invalidType();
695
+ }
696
+ }
697
+ // ---------------------------------------------------------------------------
698
+ // UPI — verbatim from upi.rs
699
+ // ---------------------------------------------------------------------------
700
+ function validateUpi(input) {
701
+ try {
702
+ const { value, error } = asString(input);
703
+ if (error)
704
+ return error;
705
+ if (value === null) {
706
+ return empty("UPI ID is empty", "UPI ID khaali hai");
707
+ }
708
+ const trimmed = value;
709
+ const parts = trimmed.split("@");
710
+ if (parts.length !== 2) {
711
+ return fail("INVALID_FORMAT", "UPI ID must be in name@handle format (e.g. name@okaxis)", "UPI ID name@handle format me hona chahiye (jaise name@okaxis)");
712
+ }
713
+ const [name, handle] = parts;
714
+ if (charsOf(name).length < 2 || charsOf(name).length > 50) {
715
+ return fail("INVALID_FORMAT", "UPI name must be 2-50 characters long", "UPI name 2 se 50 character ka hona chahiye");
716
+ }
717
+ if (!charsOf(name).every((c) => isAsciiAlnum(c) || c === "." || c === "-" || c === "_")) {
718
+ return fail("INVALID_FORMAT", "UPI name may only contain letters, digits, dot, hyphen or underscore", "UPI name me sirf akshar, digit, dot, hyphen ya underscore ho sakte hain");
719
+ }
720
+ if (handle === "") {
721
+ return fail("INVALID_HANDLE", "UPI handle is missing after @", "UPI handle @ ke baad khaali hai");
722
+ }
723
+ if (!exports.UPI_HANDLES.includes(handle.toLowerCase())) {
724
+ return fail("INVALID_HANDLE", "UPI handle is not a known handle", "UPI handle jaana-pehchana handle nahi hai");
725
+ }
726
+ return ok("UPI ID format is valid", "UPI ID ka format sahi hai", {
727
+ handle: handle.toLowerCase(),
728
+ });
729
+ }
730
+ catch {
731
+ return invalidType();
732
+ }
733
+ }
734
+ // ---------------------------------------------------------------------------
735
+ // PIN code — verbatim from pincode.rs
736
+ // ---------------------------------------------------------------------------
737
+ function validatePincode(input) {
738
+ try {
739
+ const { value, error } = asString(input);
740
+ if (error)
741
+ return error;
742
+ if (value === null) {
743
+ return empty("PIN code is empty", "PIN code khaali hai");
744
+ }
745
+ const trimmed = value;
746
+ if (charsOf(trimmed).length !== 6) {
747
+ return fail("INVALID_LENGTH", "PIN code must be exactly 6 digits", "PIN code bilkul 6 digit ka hona chahiye");
748
+ }
749
+ if (!charsOf(trimmed).every(isAsciiDigit)) {
750
+ return fail("INVALID_FORMAT", "PIN code must contain only digits", "PIN code me sirf digit hone chahiye");
751
+ }
752
+ if (!/^[1-9]/.test(trimmed)) {
753
+ return fail("INVALID_FIRST_DIGIT", "PIN code must start with 1-9", "PIN code 1 se 9 ke beech wale digit se shuru hona chahiye");
754
+ }
755
+ return ok("PIN code format is valid", "PIN code ka format sahi hai", {
756
+ pincode: trimmed,
757
+ });
758
+ }
759
+ catch {
760
+ return invalidType();
761
+ }
762
+ }
763
+ // ---------------------------------------------------------------------------
764
+ // TAN — verbatim from tan.rs
765
+ // ---------------------------------------------------------------------------
766
+ function validateTan(input) {
767
+ try {
768
+ const { value, error } = asString(input);
769
+ if (error)
770
+ return error;
771
+ if (value === null) {
772
+ return empty("TAN is empty", "TAN khaali hai");
773
+ }
774
+ const upper = asciiUpper(value);
775
+ if (charsOf(upper).length !== 10) {
776
+ return fail("INVALID_LENGTH", "TAN must be exactly 10 characters", "TAN bilkul 10 character ka hona chahiye");
777
+ }
778
+ const chars = charsOf(upper);
779
+ const formatOk = chars.slice(0, 4).every(isAsciiAlpha) &&
780
+ chars.slice(4, 9).every(isAsciiDigit) &&
781
+ isAsciiAlpha(chars[9]);
782
+ if (!formatOk) {
783
+ return fail("INVALID_FORMAT", "TAN format must be 4 letters, 5 digits, 1 letter (e.g. ABCD12345E). TAN has no checksum, so only the format is checked", "TAN ka format 4 akshar, 5 digit, 1 akshar hona chahiye (jaise ABCD12345E). TAN ka koi checksum nahi hai, isliye sirf format check hota hai");
784
+ }
785
+ return ok("TAN format is valid. TAN has no checksum, so this only checks the format", "TAN ka format sahi hai. TAN ka koi checksum nahi hai, isliye sirf format check hua hai", { tan: upper });
786
+ }
787
+ catch {
788
+ return invalidType();
789
+ }
790
+ }
791
+ // ---------------------------------------------------------------------------
792
+ // CIN — verbatim from cin.rs
793
+ // ---------------------------------------------------------------------------
794
+ function validateCin(input) {
795
+ try {
796
+ const { value, error } = asString(input);
797
+ if (error)
798
+ return error;
799
+ if (value === null) {
800
+ return empty("CIN is empty", "CIN khaali hai");
801
+ }
802
+ const upper = asciiUpper(value);
803
+ if (charsOf(upper).length !== 21) {
804
+ return fail("INVALID_LENGTH", "CIN must be exactly 21 characters", "CIN bilkul 21 character ka hona chahiye");
805
+ }
806
+ const chars = charsOf(upper);
807
+ const listingOk = chars[0] === "L" || chars[0] === "U";
808
+ const formatOk = chars.slice(1, 6).every(isAsciiDigit) &&
809
+ chars.slice(6, 8).every(isAsciiAlpha) &&
810
+ chars.slice(8, 12).every(isAsciiDigit) &&
811
+ chars.slice(12, 15).every(isAsciiAlpha) &&
812
+ chars.slice(15, 21).every(isAsciiDigit);
813
+ if (!listingOk) {
814
+ return fail("INVALID_FORMAT", "CIN must start with L (listed) or U (unlisted)", "CIN L (listed) ya U (unlisted) se shuru hona chahiye");
815
+ }
816
+ if (!formatOk) {
817
+ return fail("INVALID_FORMAT", "CIN format must be L/U + 5 digits + 2 letters + 4 digits + 3 letters + 6 digits (e.g. L12345MH2020PLC123456). CIN has no checksum, so only the format is checked", "CIN ka format L/U + 5 digit + 2 akshar + 4 digit + 3 akshar + 6 digit hona chahiye (jaise L12345MH2020PLC123456). CIN ka koi checksum nahi hai, isliye sirf format check hota hai");
818
+ }
819
+ const listing = chars[0] === "L" ? "Listed" : "Unlisted";
820
+ return ok("CIN format is valid. CIN has no checksum, so this only checks the format", "CIN ka format sahi hai. CIN ka koi checksum nahi hai, isliye sirf format check hua hai", {
821
+ cin: upper,
822
+ listing,
823
+ industry_code: chars.slice(1, 6).join(""),
824
+ state_code: chars.slice(6, 8).join(""),
825
+ year: chars.slice(8, 12).join(""),
826
+ company_type: chars.slice(12, 15).join(""),
827
+ registration_number: chars.slice(15, 21).join(""),
828
+ });
829
+ }
830
+ catch {
831
+ return invalidType();
832
+ }
833
+ }
834
+ // ---------------------------------------------------------------------------
835
+ // LLPIN — verbatim from llpin.rs
836
+ // ---------------------------------------------------------------------------
837
+ function validateLlpin(input) {
838
+ try {
839
+ const { value, error } = asString(input);
840
+ if (error)
841
+ return error;
842
+ if (value === null) {
843
+ return empty("LLPIN is empty", "LLPIN khaali hai");
844
+ }
845
+ const upper = asciiUpper(value);
846
+ if (charsOf(upper).length !== 7) {
847
+ return fail("INVALID_LENGTH", "LLPIN must be exactly 7 characters", "LLPIN bilkul 7 character ka hona chahiye");
848
+ }
849
+ const chars = charsOf(upper);
850
+ const formatOk = chars.slice(0, 3).every(isAsciiAlpha) && chars.slice(3).every(isAsciiDigit);
851
+ if (!formatOk) {
852
+ return fail("INVALID_FORMAT", "LLPIN format must be 3 letters followed by 4 digits (e.g. AAA1234). LLPIN has no checksum, so only the format is checked", "LLPIN ka format 3 akshar phir 4 digit hona chahiye (jaise AAA1234). LLPIN ka koi checksum nahi hai, isliye sirf format check hota hai");
853
+ }
854
+ return ok("LLPIN format is valid. LLPIN has no checksum, so this only checks the format", "LLPIN ka format sahi hai. LLPIN ka koi checksum nahi hai, isliye sirf format check hua hai", { llpin: upper });
855
+ }
856
+ catch {
857
+ return invalidType();
858
+ }
859
+ }
860
+ // ---------------------------------------------------------------------------
861
+ // Voter ID / EPIC — verbatim from voter.rs
862
+ // ---------------------------------------------------------------------------
863
+ function validateVoterId(input) {
864
+ try {
865
+ const { value, error } = asString(input);
866
+ if (error)
867
+ return error;
868
+ if (value === null) {
869
+ return empty("Voter ID is empty", "Voter ID khaali hai");
870
+ }
871
+ const upper = asciiUpper(value);
872
+ if (charsOf(upper).length !== 10) {
873
+ return fail("INVALID_LENGTH", "Voter ID must be exactly 10 characters", "Voter ID bilkul 10 character ka hona chahiye");
874
+ }
875
+ const chars = charsOf(upper);
876
+ const formatOk = chars.slice(0, 3).every(isAsciiAlpha) && chars.slice(3).every(isAsciiDigit);
877
+ if (!formatOk) {
878
+ return fail("INVALID_FORMAT", "Voter ID format must be 3 letters followed by 7 digits (e.g. ABC1234567). Voter ID has no checksum, so only the format is checked", "Voter ID ka format 3 akshar phir 7 digit hona chahiye (jaise ABC1234567). Voter ID ka koi checksum nahi hai, isliye sirf format check hua hai");
879
+ }
880
+ return ok("Voter ID format is valid. Voter ID has no checksum, so this only checks the format", "Voter ID ka format sahi hai. Voter ID ka koi checksum nahi hai, isliye sirf format check hua hai", { epic: upper });
881
+ }
882
+ catch {
883
+ return invalidType();
884
+ }
885
+ }
886
+ /** Alias for `validateVoterId`: EPIC is the name printed on the card. */
887
+ function validateEpic(input) {
888
+ return validateVoterId(input);
889
+ }
890
+ // ---------------------------------------------------------------------------
891
+ // Vehicle registration — verbatim from vehicle.rs
892
+ // ---------------------------------------------------------------------------
893
+ function validateVehicle(input) {
894
+ try {
895
+ const { value, error } = asString(input);
896
+ if (error)
897
+ return error;
898
+ // Unlike asString's plain trim, vehicle strips spaces/hyphens
899
+ // anywhere in the string (vehicle.rs filters them before checking).
900
+ const cleaned = [...asciiUpper(value ?? "")].filter((c) => !/\s/.test(c) && c !== "-").join("");
901
+ if (cleaned === "") {
902
+ return empty("Vehicle registration number is empty", "Vehicle registration number khaali hai");
903
+ }
904
+ const chars = charsOf(cleaned);
905
+ if (chars.length < 8 || chars.length > 10) {
906
+ return fail("INVALID_LENGTH", "Vehicle registration number must be 8-10 characters (e.g. DL01AB1234)", "Vehicle registration number 8 se 10 character ka hona chahiye (jaise DL01AB1234)");
907
+ }
908
+ // Bharat (BH) series: YYBH####XX — starts with 2 digits (the year),
909
+ // so it must be checked before the state format (2 leading letters).
910
+ if (chars.slice(0, 2).every(isAsciiDigit)) {
911
+ return validateBharat(cleaned, chars);
912
+ }
913
+ if (!chars.slice(0, 2).every(isAsciiAlpha)) {
914
+ return fail("INVALID_FORMAT", "Vehicle registration number must start with 2 state-code letters (e.g. DL01AB1234)", "Vehicle registration number 2 state-code akshar se shuru hona chahiye (jaise DL01AB1234)");
915
+ }
916
+ // Parse: district digits (1-2), series letters (1-2), number digits (1-4).
917
+ let i = 2;
918
+ const districtStart = i;
919
+ while (i < chars.length && isAsciiDigit(chars[i]) && i - districtStart < 2)
920
+ i++;
921
+ const district = chars.slice(districtStart, i).join("");
922
+ const seriesStart = i;
923
+ while (i < chars.length && isAsciiAlpha(chars[i]) && i - seriesStart < 2)
924
+ i++;
925
+ const series = chars.slice(seriesStart, i).join("");
926
+ const numberStart = i;
927
+ while (i < chars.length && isAsciiDigit(chars[i]) && i - numberStart < 4)
928
+ i++;
929
+ const number = chars.slice(numberStart, i).join("");
930
+ const formatOk = district !== "" && series !== "" && number !== "" && i === chars.length;
931
+ if (!formatOk) {
932
+ return fail("INVALID_FORMAT", "Vehicle registration format must be state letters + district digits + series letters + number digits (e.g. DL01AB1234). Registration numbers have no checksum, so only the format is checked", "Vehicle registration ka format state akshar + district digit + series akshar + number digit hona chahiye (jaise DL01AB1234). Registration number ka koi checksum nahi hai, isliye sirf format check hota hai");
933
+ }
934
+ const stateCode = chars.slice(0, 2).join("");
935
+ return ok("Vehicle registration format is valid. Registration numbers have no checksum, so this only checks the format", "Vehicle registration ka format sahi hai. Registration number ka koi checksum nahi hai, isliye sirf format check hua hai", { registration: cleaned, series_type: "state", state_code: stateCode, district, series, number });
936
+ }
937
+ catch {
938
+ return invalidType();
939
+ }
940
+ }
941
+ /** Bharat (BH) series: `YYBH####XX` — called only when the cleaned input starts with digits. */
942
+ function validateBharat(cleaned, chars) {
943
+ const notBharat = (en, hi) => fail("INVALID_FORMAT", en, hi);
944
+ const shapeOk = chars.length === 10 &&
945
+ chars[2] === "B" &&
946
+ chars[3] === "H" &&
947
+ chars.slice(4, 8).every(isAsciiDigit) &&
948
+ chars.slice(8, 10).every(isAsciiAlpha);
949
+ if (!shapeOk) {
950
+ return notBharat("Vehicle registration number must start with 2 state-code letters (e.g. DL01AB1234), or be a Bharat series number (e.g. 22BH1234AA)", "Vehicle registration number 2 state-code akshar se shuru hona chahiye (jaise DL01AB1234), ya Bharat series number hona chahiye (jaise 22BH1234AA)");
951
+ }
952
+ const year = chars.slice(0, 2).join("");
953
+ if (Number(year) < 21) {
954
+ return notBharat("Bharat (BH) series numbers start from 2021 — the year must be 21 or later (e.g. 22BH1234AA)", "Bharat (BH) series number 2021 se shuru hue hain — saal 21 ya usse baad ka hona chahiye (jaise 22BH1234AA)");
955
+ }
956
+ const number = chars.slice(4, 8).join("");
957
+ if (number === "0000") {
958
+ return notBharat("Bharat (BH) series number must be 0001-9999 (e.g. 22BH1234AA)", "Bharat (BH) series number 0001 se 9999 ke beech hona chahiye (jaise 22BH1234AA)");
959
+ }
960
+ const suffix = chars.slice(8, 10).join("");
961
+ if (suffix.includes("I") || suffix.includes("O")) {
962
+ return notBharat("Bharat (BH) series suffix letters cannot be I or O (e.g. 22BH1234AA)", "Bharat (BH) series ke aakhiri akshar I ya O nahi ho sakte (jaise 22BH1234AA)");
963
+ }
964
+ return ok("Bharat (BH) series vehicle registration format is valid. Registration numbers have no checksum, so this only checks the format", "Bharat (BH) series vehicle registration ka format sahi hai. Registration number ka koi checksum nahi hai, isliye sirf format check hua hai", { registration: cleaned, series_type: "bharat", year, series: "BH", number, suffix });
965
+ }
966
+ /** Alias for `validateVehicle`. */
967
+ function validateVehicleRegistration(input) {
968
+ return validateVehicle(input);
969
+ }
970
+ // ---------------------------------------------------------------------------
971
+ // Mock test-data generators — verbatim from generate.rs
972
+ //
973
+ // TEST DATA ONLY — syntactically valid, not real issued IDs. Every value
974
+ // produced here passes this library's own validators, but it was never
975
+ // issued by any authority (UIDAI, Income Tax, GSTN, …) and must never be
976
+ // presented as a real person's ID. Use them only in tests, demos and
977
+ // form previews. The generator is intentionally not seeded and not
978
+ // reproducible: each call may return a different value.
979
+ // ---------------------------------------------------------------------------
980
+ function randomLetter() {
981
+ return String.fromCharCode(65 + Math.floor(Math.random() * 26));
982
+ }
983
+ function randomDigit() {
984
+ return String(Math.floor(Math.random() * 10));
985
+ }
986
+ function randomDigitRange(low, high) {
987
+ return String(low + Math.floor(Math.random() * (high - low + 1)));
988
+ }
989
+ /** Generates a syntactically valid PAN for the given entity character
990
+ * (`P`, `C`, `H`, `F`, `A`, `T`, `B`, `L`, `J` or `G`).
991
+ * An unknown entity character falls back to `P` (Individual).
992
+ * TEST DATA ONLY — not a real issued PAN. */
993
+ function generatePan(entity) {
994
+ let code = "P";
995
+ if (typeof entity === "string" && entity.length > 0) {
996
+ const first = asciiUpper(entity[0]);
997
+ if (exports.PAN_ENTITY_TYPES[first] !== undefined)
998
+ code = first;
999
+ }
1000
+ return (randomLetter() +
1001
+ randomLetter() +
1002
+ randomLetter() +
1003
+ code +
1004
+ randomLetter() +
1005
+ randomDigit() +
1006
+ randomDigit() +
1007
+ randomDigit() +
1008
+ randomDigit() +
1009
+ randomLetter());
1010
+ }
1011
+ /** Generates a syntactically valid 12-digit Aadhaar number (first digit
1012
+ * 2-9, correct Verhoeff check digit).
1013
+ * TEST DATA ONLY — not a real issued Aadhaar number. */
1014
+ function generateAadhaar() {
1015
+ let base = randomDigitRange(2, 9);
1016
+ for (let i = 0; i < 10; i++)
1017
+ base += randomDigit();
1018
+ return base + verhoeffVerifyDigit(base);
1019
+ }
1020
+ /** Generates a syntactically valid GSTIN for the given 2-digit state code
1021
+ * (e.g. `"18"` for Assam). An unknown state code falls back to `"18"`.
1022
+ * TEST DATA ONLY — not a real issued GSTIN. */
1023
+ function generateGstin(stateCode) {
1024
+ let code = "18";
1025
+ if (typeof stateCode === "string" && exports.GSTIN_STATE_NAMES[stateCode] !== undefined) {
1026
+ code = stateCode;
1027
+ }
1028
+ const pan = generatePan("P");
1029
+ const entityChar = Math.random() < 0.5 ? "1" : "2";
1030
+ const first14 = `${code}${pan}${entityChar}Z`;
1031
+ const check = gstinCheckDigit(first14) ?? "0";
1032
+ return first14 + check;
1033
+ }
1034
+ /** Generates a syntactically valid Indian mobile number (10 digits,
1035
+ * first digit 6-9). TEST DATA ONLY — not a real person's number. */
1036
+ function generateMobile() {
1037
+ let mobile = randomDigitRange(6, 9);
1038
+ for (let i = 0; i < 9; i++)
1039
+ mobile += randomDigit();
1040
+ return mobile;
1041
+ }
1042
+ const VALIDATORS = {
1043
+ mobile: validateMobile,
1044
+ pan: validatePan,
1045
+ aadhaar: validateAadhaar,
1046
+ gstin: validateGstin,
1047
+ ifsc: validateIfsc,
1048
+ upi: validateUpi,
1049
+ pincode: validatePincode,
1050
+ tan: validateTan,
1051
+ cin: validateCin,
1052
+ llpin: validateLlpin,
1053
+ voter: validateVoterId,
1054
+ vehicle: validateVehicle,
1055
+ };
1056
+ function isPanPattern(s) {
1057
+ const chars = charsOf(s);
1058
+ return (chars.length === 10 &&
1059
+ chars.slice(0, 5).every(isAsciiAlpha) &&
1060
+ chars.slice(5, 9).every(isAsciiDigit) &&
1061
+ isAsciiAlpha(chars[9]));
1062
+ }
1063
+ function isIfscPattern(s) {
1064
+ const chars = charsOf(s);
1065
+ return (chars.length === 11 &&
1066
+ chars.slice(0, 4).every(isAsciiAlpha) &&
1067
+ chars[4] === "0" &&
1068
+ chars.slice(5).every(isAsciiAlnum));
1069
+ }
1070
+ function isTanPattern(s) {
1071
+ const chars = charsOf(s);
1072
+ return (chars.length === 10 &&
1073
+ chars.slice(0, 4).every(isAsciiAlpha) &&
1074
+ chars.slice(4, 9).every(isAsciiDigit) &&
1075
+ isAsciiAlpha(chars[9]));
1076
+ }
1077
+ function isVoterPattern(s) {
1078
+ const chars = charsOf(s);
1079
+ return (chars.length === 10 &&
1080
+ chars.slice(0, 3).every(isAsciiAlpha) &&
1081
+ chars.slice(3).every(isAsciiDigit));
1082
+ }
1083
+ function isCinPattern(s) {
1084
+ const chars = charsOf(s);
1085
+ return (chars.length === 21 &&
1086
+ (chars[0] === "L" || chars[0] === "U") &&
1087
+ chars.slice(1, 6).every(isAsciiDigit) &&
1088
+ chars.slice(6, 8).every(isAsciiAlpha) &&
1089
+ chars.slice(8, 12).every(isAsciiDigit) &&
1090
+ chars.slice(12, 15).every(isAsciiAlpha) &&
1091
+ chars.slice(15, 21).every(isAsciiDigit));
1092
+ }
1093
+ function isLlpinPattern(s) {
1094
+ const chars = charsOf(s);
1095
+ return (chars.length === 7 &&
1096
+ chars.slice(0, 3).every(isAsciiAlpha) &&
1097
+ chars.slice(3).every(isAsciiDigit));
1098
+ }
1099
+ function isVehiclePattern(s) {
1100
+ // Cheap structural check used only by detect(); the real validation
1101
+ // (district/series/number parsing) lives in validateVehicle.
1102
+ const cleaned = [...s].filter((c) => !/\s/.test(c) && c !== "-").join("");
1103
+ return validateVehicle(cleaned).valid;
1104
+ }
1105
+ /** The Rust `detect` string for a trimmed input (lib.rs order). */
1106
+ function detectType(input) {
1107
+ const trimmed = input.trim();
1108
+ if (trimmed === "")
1109
+ return "unknown";
1110
+ if (trimmed.includes("@"))
1111
+ return "upi";
1112
+ const upper = asciiUpper(trimmed);
1113
+ const len = charsOf(trimmed).length;
1114
+ const allDigits = charsOf(trimmed).every(isAsciiDigit);
1115
+ // A Verhoeff-valid 12-digit number is an Aadhaar even if it also looks
1116
+ // like a 91-prefixed mobile; otherwise a normalisable mobile wins over
1117
+ // the length-15 GSTIN branch (e.g. "+91 98765 43210" is 15 chars long).
1118
+ if (len === 12 && allDigits && verhoeffValid(trimmed))
1119
+ return "aadhaar";
1120
+ // Spaced / hyphenated Aadhaar, e.g. "2345 6789 0124".
1121
+ const digitsOnly = charsOf(trimmed).filter(isAsciiDigit).join("");
1122
+ if (charsOf(digitsOnly).length === 12 && verhoeffValid(digitsOnly))
1123
+ return "aadhaar";
1124
+ if (normalizeMobile(trimmed) !== null)
1125
+ return "mobile";
1126
+ // CIN is 21 chars and unambiguous by length.
1127
+ if (len === 21 && isCinPattern(upper))
1128
+ return "cin";
1129
+ switch (len) {
1130
+ case 15:
1131
+ return "gstin";
1132
+ case 12:
1133
+ if (allDigits)
1134
+ return "aadhaar";
1135
+ break;
1136
+ case 11:
1137
+ if (isIfscPattern(upper))
1138
+ return "ifsc";
1139
+ break;
1140
+ case 10:
1141
+ if (isPanPattern(upper))
1142
+ return "pan";
1143
+ if (isTanPattern(upper))
1144
+ return "tan";
1145
+ if (isVoterPattern(upper))
1146
+ return "voter";
1147
+ if (allDigits)
1148
+ return "mobile";
1149
+ break;
1150
+ case 7:
1151
+ if (isLlpinPattern(upper))
1152
+ return "llpin";
1153
+ break;
1154
+ case 6:
1155
+ if (allDigits)
1156
+ return "pincode";
1157
+ break;
1158
+ default:
1159
+ break;
1160
+ }
1161
+ // Formatted mobiles such as "+91 98765 43210" are longer than 10 chars.
1162
+ if (formatMobileString(trimmed, exports.DEFAULT_OPTIONS) !== null)
1163
+ return "mobile";
1164
+ if (isPanPattern(upper))
1165
+ return "pan";
1166
+ if (isTanPattern(upper))
1167
+ return "tan";
1168
+ if (isVoterPattern(upper))
1169
+ return "voter";
1170
+ if (isIfscPattern(upper))
1171
+ return "ifsc";
1172
+ if (isLlpinPattern(upper))
1173
+ return "llpin";
1174
+ if (isVehiclePattern(upper))
1175
+ return "vehicle";
1176
+ return "unknown";
1177
+ }
1178
+ function detect(input) {
1179
+ try {
1180
+ if (typeof input !== "string") {
1181
+ return fail("INVALID_TYPE", "Input must be a string.", "Input string hona chahiye.", {
1182
+ type: null,
1183
+ value: null,
1184
+ result: null,
1185
+ });
1186
+ }
1187
+ const trimmed = input.trim();
1188
+ const type = detectType(input);
1189
+ if (type === "unknown") {
1190
+ return fail("UNKNOWN_TYPE", "Could not detect the type of this input", "Is input ka type pehchana nahi ja saka", { type: "unknown", value: trimmed, result: null });
1191
+ }
1192
+ const result = VALIDATORS[type](input);
1193
+ return {
1194
+ valid: result.valid,
1195
+ code: result.code,
1196
+ reason_en: result.reason_en,
1197
+ reason_hi: result.reason_hi,
1198
+ parsed: { type, value: trimmed, result },
1199
+ };
1200
+ }
1201
+ catch {
1202
+ return fail("INVALID_TYPE", "Input must be a string.", "Input string hona chahiye.", {
1203
+ type: null,
1204
+ value: null,
1205
+ result: null,
1206
+ });
1207
+ }
1208
+ }
1209
+ function validateMany(inputs) {
1210
+ try {
1211
+ if (!Array.isArray(inputs)) {
1212
+ return fail("INVALID_TYPE", "Input must be a string.", "Input string hona chahiye.", { results: [], total: 0, validCount: 0, invalidCount: 0 });
1213
+ }
1214
+ if (inputs.length === 0) {
1215
+ return fail("EMPTY", "Input is empty.", "Input khaali hai.", {
1216
+ results: [],
1217
+ total: 0,
1218
+ validCount: 0,
1219
+ invalidCount: 0,
1220
+ });
1221
+ }
1222
+ // Mirrors Rust `validate_many`: each input is detected, then validated
1223
+ // with the matching validator. An undetectable input gets UNKNOWN_TYPE
1224
+ // with the Rust reason strings.
1225
+ const results = inputs.map((item) => {
1226
+ let result;
1227
+ let type;
1228
+ if (item !== null &&
1229
+ typeof item === "object" &&
1230
+ "value" in item &&
1231
+ typeof item.type === "string" &&
1232
+ item.type in VALIDATORS) {
1233
+ const typed = item;
1234
+ type = typed.type;
1235
+ result = VALIDATORS[typed.type](typed.value);
1236
+ }
1237
+ else if (typeof item === "string") {
1238
+ const detected = detectType(item);
1239
+ type = detected === "unknown" ? null : detected;
1240
+ result =
1241
+ detected === "unknown"
1242
+ ? fail("UNKNOWN_TYPE", "Could not detect the type of this input", "Is input ka type pehchana nahi ja saka")
1243
+ : VALIDATORS[detected](item);
1244
+ }
1245
+ else {
1246
+ type = null;
1247
+ result = invalidType();
1248
+ }
1249
+ return {
1250
+ input: item,
1251
+ type,
1252
+ valid: result.valid,
1253
+ code: result.code,
1254
+ reason_en: result.reason_en,
1255
+ reason_hi: result.reason_hi,
1256
+ parsed: result.parsed,
1257
+ };
1258
+ });
1259
+ const validCount = results.filter((r) => r.valid).length;
1260
+ const invalidCount = results.length - validCount;
1261
+ const parsed = { results, total: results.length, validCount, invalidCount };
1262
+ if (invalidCount === 0) {
1263
+ return ok("All inputs are valid.", "Saare input sahi hain.", parsed);
1264
+ }
1265
+ // Wrapper-only code: the Rust core returns a bare Vec for batches.
1266
+ return fail("SOME_INVALID", "Some inputs are invalid.", "Kuch input galat hain.", parsed);
1267
+ }
1268
+ catch {
1269
+ return fail("INVALID_TYPE", "Input must be a string.", "Input string hona chahiye.", { results: [], total: 0, validCount: 0, invalidCount: 0 });
1270
+ }
1271
+ }
1272
+ exports.default = {
1273
+ validateMobile,
1274
+ validatePan,
1275
+ validateAadhaar,
1276
+ validateGstin,
1277
+ validateIfsc,
1278
+ validateUpi,
1279
+ validatePincode,
1280
+ validateTan,
1281
+ validateCin,
1282
+ validateLlpin,
1283
+ validateVoterId,
1284
+ validateEpic,
1285
+ validateVehicle,
1286
+ validateVehicleRegistration,
1287
+ validateMobileWithOptions,
1288
+ validatePanWithOptions,
1289
+ validateAadhaarWithOptions,
1290
+ validateGstinWithOptions,
1291
+ formatMobile,
1292
+ formatMobileWithOptions,
1293
+ formatPan,
1294
+ formatGstin,
1295
+ maskAadhaar,
1296
+ maskPan,
1297
+ maskMobile,
1298
+ generatePan,
1299
+ generateAadhaar,
1300
+ generateGstin,
1301
+ generateMobile,
1302
+ defaultOptions,
1303
+ lenientOptions,
1304
+ noSpacesHyphensOptions,
1305
+ detect,
1306
+ detectType,
1307
+ validateMany,
1308
+ UPI_HANDLES: exports.UPI_HANDLES,
1309
+ PAN_ENTITY_TYPES: exports.PAN_ENTITY_TYPES,
1310
+ GSTIN_STATE_NAMES: exports.GSTIN_STATE_NAMES,
1311
+ };