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/README.md +117 -2
- package/dist/index.d.ts +302 -0
- package/dist/index.js +1311 -0
- package/dist/zod.d.ts +41 -0
- package/dist/zod.js +43 -0
- package/package.json +38 -3
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
|
+
};
|