@marslanmustafa/input-shield 0.1.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.
@@ -0,0 +1,520 @@
1
+ 'use strict';
2
+
3
+ var __defProp = Object.defineProperty;
4
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
6
+
7
+ // src/core/normalize.ts
8
+ var HOMOGLYPH_MAP = {
9
+ // Cyrillic → Latin (most common attack vector)
10
+ "\u0430": "a",
11
+ // а → a
12
+ "\u0435": "e",
13
+ // е → e
14
+ "\u0440": "r",
15
+ // р → r
16
+ "\u0441": "c",
17
+ // с → c
18
+ "\u043E": "o",
19
+ // о → o
20
+ "\u0445": "x",
21
+ // х → x
22
+ "\u0443": "y",
23
+ // у → y
24
+ "\u0456": "i",
25
+ // і → i
26
+ "\u0432": "b",
27
+ // в (approximate) → b
28
+ "\u0417": "z",
29
+ // З → z (3-lookalike)
30
+ "\u0421": "c",
31
+ // С → C
32
+ "\u0410": "a",
33
+ // А → a
34
+ "\u0412": "b",
35
+ // В → b
36
+ "\u0415": "e",
37
+ // Е → e
38
+ "\u041C": "m",
39
+ // М → m
40
+ "\u041D": "h",
41
+ // Н → h
42
+ "\u041E": "o",
43
+ // О → o
44
+ "\u0420": "r",
45
+ // Р → r
46
+ "\u0422": "t",
47
+ // Т → t
48
+ "\u0425": "x",
49
+ // Х → x
50
+ "\u0423": "y",
51
+ // У → y
52
+ // Greek → Latin
53
+ "\u03B1": "a",
54
+ // α → a
55
+ "\u03B2": "b",
56
+ // β → b (approximate)
57
+ "\u03B5": "e",
58
+ // ε → e
59
+ "\u03B9": "i",
60
+ // ι → i
61
+ "\u03BA": "k",
62
+ // κ → k
63
+ "\u03BD": "v",
64
+ // ν → v
65
+ "\u03BF": "o",
66
+ // ο → o
67
+ "\u03C1": "p",
68
+ // ρ → p
69
+ "\u03C5": "u",
70
+ // υ → u
71
+ "\u03C7": "x",
72
+ // χ → x
73
+ "\u0391": "a",
74
+ // Α → a
75
+ "\u0392": "b",
76
+ // Β → b
77
+ "\u0395": "e",
78
+ // Ε → e
79
+ "\u0396": "z",
80
+ // Ζ → z
81
+ "\u0397": "h",
82
+ // Η → h
83
+ "\u0399": "i",
84
+ // Ι → i
85
+ "\u039A": "k",
86
+ // Κ → k
87
+ "\u039C": "m",
88
+ // Μ → m
89
+ "\u039D": "n",
90
+ // Ν → n
91
+ "\u039F": "o",
92
+ // Ο → o
93
+ "\u03A1": "r",
94
+ // Ρ → r
95
+ "\u03A4": "t",
96
+ // Τ → t
97
+ "\u03A5": "y",
98
+ // Υ → y
99
+ "\u03A7": "x",
100
+ // Χ → x
101
+ // Armenian → Latin
102
+ "\u0570": "h",
103
+ // հ → h
104
+ "\u0578": "o",
105
+ // օ → o (approximate)
106
+ // Other common confusables
107
+ "\u2044": "/",
108
+ // ⁄ fraction slash → /
109
+ "\u2215": "/",
110
+ // ∕ division slash → /
111
+ "\u01A0": "o",
112
+ // Ơ → o
113
+ "\u0D20": "t"
114
+ // ഠ → t (approximate)
115
+ };
116
+ var LEET_MAP = {
117
+ "0": "o",
118
+ "1": "i",
119
+ "2": "z",
120
+ "3": "e",
121
+ "4": "a",
122
+ "5": "s",
123
+ "6": "g",
124
+ "7": "t",
125
+ "8": "b",
126
+ "9": "g",
127
+ "@": "a",
128
+ "$": "s",
129
+ "!": "i",
130
+ "+": "t",
131
+ "|": "l",
132
+ "(": "c"
133
+ };
134
+ function stripSeparators(t) {
135
+ return t.replace(/([a-z0-9])[.\-_|\\\/](?=[a-z0-9])/gi, "$1");
136
+ }
137
+ function toSkeleton(t) {
138
+ if (!t) return "";
139
+ let s = t.normalize("NFKC");
140
+ s = stripSeparators(s);
141
+ s = s.split("").map((c) => HOMOGLYPH_MAP[c] ?? c).join("");
142
+ s = s.toLowerCase();
143
+ s = s.split("").map((c) => LEET_MAP[c] ?? c).join("");
144
+ s = s.replace(/[^a-z0-9\s]/g, "");
145
+ return s.replace(/\s+/g, " ").trim();
146
+ }
147
+ function toStructural(t) {
148
+ if (!t) return "";
149
+ return t.normalize("NFKC").replace(/\s+/g, " ").trim();
150
+ }
151
+
152
+ // src/core/profanity.ts
153
+ var PROFANITY_PATTERNS = [
154
+ // fuck — also matches 'fack' because leet '4' → 'a', so we allow u OR a in slot 2
155
+ /\bf+[ua]+c+k+(e[dr]|ing|s|er)?\b/i,
156
+ /\bs+h+i+t+(s|te[dr]|ting)?\b/i,
157
+ /\bb+i+t+c+h+(e[sd]|ing)?\b/i,
158
+ // ass — use (^|\s|[^a-z]) instead of \b so it catches "@ss" → "ass" at start of string
159
+ /(?:^|(?<=[^a-z]))a+s{2,}(h+o+l+e+s?|e[sd]|ing)?\b/i,
160
+ /\bc+u+n+t+s?\b/i,
161
+ // dick — exclude as a proper first name before a capitalized surname
162
+ /\bd+i+c+k+(s|ed|ing)?\b(?! [A-Z])/i,
163
+ /\bp+r+i+c+k+s?\b/i,
164
+ /\bb+a+s+t+a+r+d+s?\b/i,
165
+ /\bw+h+o+r+e+s?\b/i,
166
+ /\bf+a+g+(g+o+t+s?)?\b/i,
167
+ /\bn+i+g+(g+e+r+s?|ga+s?)?\b/i,
168
+ /\bfrick\b/i
169
+ ];
170
+ function containsProfanity(text) {
171
+ const skeleton = toSkeleton(text);
172
+ return PROFANITY_PATTERNS.some((p) => p.test(skeleton));
173
+ }
174
+ function getMatchedProfanityPattern(text) {
175
+ const skeleton = toSkeleton(text);
176
+ return PROFANITY_PATTERNS.find((p) => p.test(skeleton)) ?? null;
177
+ }
178
+
179
+ // src/core/spam.ts
180
+ var KEYWORD_PATTERNS = [
181
+ /\bviagra\b/i,
182
+ /\bcialis\b/i,
183
+ /\bcasino\b/i,
184
+ /\bpoker\b/i,
185
+ /\bfree\s+money\b/i,
186
+ // skeleton preserves spaces between words
187
+ /\bbuy\s+now\b/i,
188
+ /\bclick\s+here\b/i,
189
+ /\blorem\s+ipsum\b/i,
190
+ /\bwork\s+from\s+home\b/i,
191
+ /\bmake\s+money\b/i,
192
+ /\bget\s+rich\b/i,
193
+ /\bonlyfans\b/i
194
+ // skeleton collapses "only fans" → "only fans" but domain is onlyfans
195
+ ];
196
+ var URL_PATTERNS = [
197
+ // Full URLs
198
+ /https?:\/\/[^\s]{4,}/i,
199
+ // Bare domains with common TLDs (not preceded by a letter — avoids "version2.0")
200
+ /(?<![a-z])\b[a-z0-9]([a-z0-9-]{1,61})\.(com|net|org|io|xyz|ru|cn|co|info|biz|me|gg|app|dev|uk|de|fr)\b/i,
201
+ // IP addresses
202
+ /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/
203
+ ];
204
+ function containsSpam(text) {
205
+ const skeleton = toSkeleton(text);
206
+ const nfkc = text.normalize("NFKC");
207
+ if (KEYWORD_PATTERNS.some((p) => p.test(nfkc) || p.test(skeleton))) return true;
208
+ if (URL_PATTERNS.some((p) => p.test(nfkc))) return true;
209
+ return false;
210
+ }
211
+
212
+ // src/core/gibberish.ts
213
+ var GIBBERISH_ALLOWLIST = /* @__PURE__ */ new Set([
214
+ "rhythm",
215
+ "rhythms",
216
+ "krzysztof",
217
+ "szczepanski",
218
+ "grzegorz",
219
+ "przemek",
220
+ "strength",
221
+ "strengths",
222
+ "lymph",
223
+ "nymph",
224
+ "nymphs",
225
+ "glyph",
226
+ "glyphs",
227
+ "crypt",
228
+ "crypts",
229
+ "tryst",
230
+ "pygmy",
231
+ "synth",
232
+ "psych"
233
+ ]);
234
+ var CONFIGS = {
235
+ loose: {
236
+ consonantRun: 7,
237
+ vowelRatioMin: 0.05,
238
+ vowelRatioWordLen: 12,
239
+ noVowelWordLen: 6
240
+ },
241
+ normal: {
242
+ consonantRun: 6,
243
+ vowelRatioMin: 0.1,
244
+ vowelRatioWordLen: 8,
245
+ noVowelWordLen: 5
246
+ },
247
+ strict: {
248
+ consonantRun: 5,
249
+ vowelRatioMin: 0.15,
250
+ vowelRatioWordLen: 6,
251
+ noVowelWordLen: 4
252
+ }
253
+ };
254
+ var CONSONANT_RUN_PATTERNS = {
255
+ loose: /[bcdfghjklmnpqrstvwxyz]{7,}/i,
256
+ normal: /[bcdfghjklmnpqrstvwxyz]{6,}/i,
257
+ strict: /[bcdfghjklmnpqrstvwxyz]{5,}/i
258
+ };
259
+ function isWordGibberish(word, sensitivity) {
260
+ if (word.length > 25) return true;
261
+ if (GIBBERISH_ALLOWLIST.has(word)) return false;
262
+ const cfg = CONFIGS[sensitivity];
263
+ if (CONSONANT_RUN_PATTERNS[sensitivity].test(word)) return true;
264
+ if (word.length >= cfg.vowelRatioWordLen) {
265
+ const vowels = (word.match(/[aeiou]/g) ?? []).length;
266
+ if (vowels / word.length < cfg.vowelRatioMin) return true;
267
+ }
268
+ if (word.length >= cfg.noVowelWordLen) {
269
+ const vowels = (word.match(/[aeiou]/g) ?? []).length;
270
+ if (vowels === 0) return true;
271
+ }
272
+ return false;
273
+ }
274
+ function isGibberish(text, sensitivity = "normal") {
275
+ const skeleton = toSkeleton(text);
276
+ const words = skeleton.split(" ").filter((w) => w.length >= 4);
277
+ if (words.length === 0) return false;
278
+ return words.some((word) => isWordGibberish(word, sensitivity));
279
+ }
280
+ function hasRepeatingChars(text) {
281
+ return /(.)\1{4,}/.test(toSkeleton(text));
282
+ }
283
+
284
+ // src/core/structure.ts
285
+ function hasExcessiveSymbols(text) {
286
+ const s = toStructural(text);
287
+ if (s.length < 5) return false;
288
+ const symbols = (s.match(/[^a-zA-Z0-9\s]/g) ?? []).length;
289
+ return symbols / s.length > 0.4;
290
+ }
291
+ function hasLowAlphabetRatio(text) {
292
+ const s = toStructural(text);
293
+ const letters = (s.match(/[a-zA-Z]/g) ?? []).length;
294
+ if (s.length <= 5) return letters === 0;
295
+ if (letters < 3) return true;
296
+ if (s.length >= 15 && letters / s.length < 0.2) return true;
297
+ return false;
298
+ }
299
+ function hasRepeatedContentWords(text) {
300
+ const s = toStructural(text).toLowerCase();
301
+ const words = s.replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 3);
302
+ const seen = /* @__PURE__ */ new Set();
303
+ let repeatCount = 0;
304
+ for (const word of words) {
305
+ if (seen.has(word)) repeatCount++;
306
+ seen.add(word);
307
+ }
308
+ return repeatCount >= 2;
309
+ }
310
+ var LOW_EFFORT_SET = /* @__PURE__ */ new Set([
311
+ "test",
312
+ "testing",
313
+ "tester",
314
+ "demo",
315
+ "sample",
316
+ "trial",
317
+ "asdf",
318
+ "qwer",
319
+ "zxcv",
320
+ "qwerty",
321
+ "qwertyuiop",
322
+ "placeholder",
323
+ "foo",
324
+ "bar",
325
+ "baz",
326
+ "foobar",
327
+ "helloworld",
328
+ // "hello world" after skeleton strip
329
+ "loremipsum",
330
+ // same
331
+ "aaa",
332
+ "bbb",
333
+ // caught by repeatingChars too, belt-and-suspenders
334
+ "xxx",
335
+ "yyy",
336
+ "zzz",
337
+ "na",
338
+ "none",
339
+ "null",
340
+ "undefined",
341
+ "nope",
342
+ "no",
343
+ "n/a"
344
+ ]);
345
+ function isLowEffortExact(skeletonText) {
346
+ const noSpaces = skeletonText.replace(/\s/g, "");
347
+ return LOW_EFFORT_SET.has(skeletonText) || LOW_EFFORT_SET.has(noSpaces);
348
+ }
349
+
350
+ // src/validators/builder.ts
351
+ var InputShieldValidator = class {
352
+ constructor() {
353
+ __publicField(this, "_fieldName", "Input");
354
+ __publicField(this, "_min", 2);
355
+ __publicField(this, "_max", 500);
356
+ __publicField(this, "_allowlist", /* @__PURE__ */ new Set());
357
+ __publicField(this, "_checks", []);
358
+ __publicField(this, "_spamCheck", null);
359
+ }
360
+ // ─── Configuration ──────────────────────────────────────────────────────────
361
+ /** Set the field label used in error messages */
362
+ field(name) {
363
+ this._fieldName = name;
364
+ return this;
365
+ }
366
+ /** Minimum character length (post-trim). Default: 2 */
367
+ min(n) {
368
+ this._min = n;
369
+ return this;
370
+ }
371
+ /** Maximum character length. Default: 500 */
372
+ max(n) {
373
+ this._max = n;
374
+ return this;
375
+ }
376
+ /**
377
+ * Add strings that always pass validation, regardless of other checks.
378
+ * Useful for brand names, tech terms, or known-good short strings.
379
+ * .allow('nginx', 'kubectl', 'QA', 'IT')
380
+ */
381
+ allow(...words) {
382
+ words.forEach((w) => this._allowlist.add(toSkeleton(w)));
383
+ return this;
384
+ }
385
+ // ─── Check methods ──────────────────────────────────────────────────────────
386
+ /** Block profanity, including leet-speak and homoglyph evasions */
387
+ noProfanity() {
388
+ this._checks.push(
389
+ (_raw, skeleton) => containsProfanity(skeleton) ? { reason: "profanity", message: "contains inappropriate language." } : null
390
+ );
391
+ return this;
392
+ }
393
+ /**
394
+ * Block spam keywords and URLs.
395
+ * Note: URL detection uses the raw string internally (not the skeleton),
396
+ * so you don't need to worry about URLs being missed.
397
+ */
398
+ noSpam() {
399
+ const spamFn = (raw) => containsSpam(raw) ? { reason: "spam", message: "appears to contain spam or promotional content." } : null;
400
+ this._spamCheck = spamFn;
401
+ this._checks.push(spamFn);
402
+ return this;
403
+ }
404
+ /**
405
+ * Block gibberish / keyboard mash.
406
+ *
407
+ * @param options.sensitivity
408
+ * 'loose' — only catches extreme mashing (7+ consonants). Safe for names.
409
+ * 'normal' — default. Catches most mashing while allowing tech words.
410
+ * 'strict' — also catches low vowel-ratio words. Best for usernames.
411
+ */
412
+ noGibberish(options = {}) {
413
+ const sensitivity = options.sensitivity ?? "normal";
414
+ this._checks.push((raw) => {
415
+ if (hasRepeatingChars(raw) || isGibberish(raw, sensitivity)) {
416
+ return { reason: "gibberish", message: "appears to be gibberish or keyboard mash." };
417
+ }
418
+ return null;
419
+ });
420
+ return this;
421
+ }
422
+ /** Block inputs that are structurally low quality (too many symbols, too few letters) */
423
+ noLowQuality() {
424
+ this._checks.push((raw, skeleton) => {
425
+ if (hasExcessiveSymbols(raw)) {
426
+ return { reason: "excessive_symbols", message: "contains too many special characters." };
427
+ }
428
+ if (hasLowAlphabetRatio(raw)) {
429
+ return { reason: "low_effort", message: "must contain more letters." };
430
+ }
431
+ if (isLowEffortExact(skeleton)) {
432
+ return { reason: "low_effort", message: "appears to be a placeholder or filler value." };
433
+ }
434
+ return null;
435
+ });
436
+ return this;
437
+ }
438
+ /** Block inputs that repeat content words excessively */
439
+ noRepeatedWords() {
440
+ this._checks.push(
441
+ (raw) => hasRepeatedContentWords(raw) ? { reason: "low_effort", message: "contains too many repeated words." } : null
442
+ );
443
+ return this;
444
+ }
445
+ /**
446
+ * Add a custom validation rule.
447
+ *
448
+ * @param fn - Returns true if the input is INVALID (should be blocked)
449
+ * @param reason - The FailReason code to return
450
+ * @param message - Human-readable message (do not include fieldName, it's prepended)
451
+ *
452
+ * @example
453
+ * .custom(t => t.includes('@'), 'custom', 'product names cannot contain @')
454
+ */
455
+ custom(fn, reason, message) {
456
+ this._checks.push(
457
+ (raw) => fn(raw) ? { reason, message } : null
458
+ );
459
+ return this;
460
+ }
461
+ // ─── Validation ─────────────────────────────────────────────────────────────
462
+ validate(text) {
463
+ const raw = (text ?? "").trim();
464
+ const skeleton = toSkeleton(raw);
465
+ const f = this._fieldName;
466
+ if (this._allowlist.has(skeleton)) {
467
+ return { isValid: true };
468
+ }
469
+ if (!raw) {
470
+ return { isValid: false, reason: "empty", message: `${f} cannot be empty.` };
471
+ }
472
+ const displayLength = toStructural(raw).length;
473
+ if (displayLength < this._min) {
474
+ return { isValid: false, reason: "too_short", message: `${f} must be at least ${this._min} characters.` };
475
+ }
476
+ if (displayLength > this._max) {
477
+ return { isValid: false, reason: "too_long", message: `${f} must be no more than ${this._max} characters.` };
478
+ }
479
+ const spamCheck = this._checks.find((c) => c === this._spamCheck);
480
+ if (spamCheck) {
481
+ const result = spamCheck(raw, skeleton);
482
+ if (result) return { isValid: false, reason: result.reason, message: `${f} ${result.message}` };
483
+ }
484
+ for (const check of this._checks) {
485
+ if (check === this._spamCheck) continue;
486
+ const result = check(raw, skeleton);
487
+ if (result) {
488
+ return { isValid: false, reason: result.reason, message: `${f} ${result.message}` };
489
+ }
490
+ }
491
+ return { isValid: true };
492
+ }
493
+ /**
494
+ * Validate and throw if invalid. Useful in form libraries / Zod .superRefine().
495
+ * @throws Error with the validation message
496
+ */
497
+ validateOrThrow(text) {
498
+ const result = this.validate(text);
499
+ if (!result.isValid) throw new Error(result.message);
500
+ }
501
+ };
502
+ function createValidator() {
503
+ return new InputShieldValidator();
504
+ }
505
+
506
+ exports.InputShieldValidator = InputShieldValidator;
507
+ exports.containsProfanity = containsProfanity;
508
+ exports.containsSpam = containsSpam;
509
+ exports.createValidator = createValidator;
510
+ exports.getMatchedProfanityPattern = getMatchedProfanityPattern;
511
+ exports.hasExcessiveSymbols = hasExcessiveSymbols;
512
+ exports.hasLowAlphabetRatio = hasLowAlphabetRatio;
513
+ exports.hasRepeatedContentWords = hasRepeatedContentWords;
514
+ exports.hasRepeatingChars = hasRepeatingChars;
515
+ exports.isGibberish = isGibberish;
516
+ exports.isLowEffortExact = isLowEffortExact;
517
+ exports.toSkeleton = toSkeleton;
518
+ exports.toStructural = toStructural;
519
+ //# sourceMappingURL=chunk-67CBN3U4.cjs.map
520
+ //# sourceMappingURL=chunk-67CBN3U4.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/normalize.ts","../src/core/profanity.ts","../src/core/spam.ts","../src/core/gibberish.ts","../src/core/structure.ts","../src/validators/builder.ts"],"names":[],"mappings":"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","file":"chunk-67CBN3U4.cjs","sourcesContent":["/**\n * normalize.ts\n *\n * THREE-STAGE normalization pipeline.\n * This is THE most important file in the package.\n *\n * Stage 1 — Unicode NFKC:\n * Collapses compatibility variants before anything else.\n * \"A\" (fullwidth) → \"A\", \"fi\" (ligature) → \"fi\", \"𝐅\" (math bold) → \"F\"\n * This also handles bidirectional control characters and zero-width spaces.\n *\n * Stage 2 — Homoglyph map:\n * Catches Cyrillic/Greek/Armenian lookalikes that survive NFKC.\n * \"о\" (U+043E Cyrillic) → \"o\", \"а\" (U+0430 Cyrillic) → \"a\"\n * This is the CVE-2025-27611 class of bypass — NFKC alone doesn't fix it.\n *\n * Stage 3 — Leet-speak map:\n * Classic ASCII substitutions: \"3\" → \"e\", \"@\" → \"a\", \"$\" → \"s\", etc.\n * Runs LAST so homoglyphs don't interfere with leet detection.\n *\n * Result: \"P.0.r.n\" → \"porn\", \"ſhit\" → \"shit\", \"аss\" (Cyrillic а) → \"ass\"\n *\n * IMPORTANT: This output is ONLY for pattern matching — never display it.\n * Always return error messages that reference the original input.\n */\n\n// ─── Stage 2: Homoglyph / confusable map ─────────────────────────────────────\n// Subset of Unicode TR39 confusables.txt, filtered to high-risk lookalikes.\n// Full table would be ~40 KB; this targeted map is <2 KB and covers 95% of\n// real-world bypass attempts (Cyrillic, Greek, Armenian to Latin).\nconst HOMOGLYPH_MAP: Record<string, string> = {\n // Cyrillic → Latin (most common attack vector)\n '\\u0430': 'a', // а → a\n '\\u0435': 'e', // е → e\n '\\u0440': 'r', // р → r\n '\\u0441': 'c', // с → c\n '\\u043E': 'o', // о → o\n '\\u0445': 'x', // х → x\n '\\u0443': 'y', // у → y\n '\\u0456': 'i', // і → i\n '\\u0432': 'b', // в (approximate) → b\n '\\u0417': 'z', // З → z (3-lookalike)\n '\\u0421': 'c', // С → C\n '\\u0410': 'a', // А → a\n '\\u0412': 'b', // В → b\n '\\u0415': 'e', // Е → e\n '\\u041C': 'm', // М → m\n '\\u041D': 'h', // Н → h\n '\\u041E': 'o', // О → o\n '\\u0420': 'r', // Р → r\n '\\u0422': 't', // Т → t\n '\\u0425': 'x', // Х → x\n '\\u0423': 'y', // У → y\n\n // Greek → Latin\n '\\u03B1': 'a', // α → a\n '\\u03B2': 'b', // β → b (approximate)\n '\\u03B5': 'e', // ε → e\n '\\u03B9': 'i', // ι → i\n '\\u03BA': 'k', // κ → k\n '\\u03BD': 'v', // ν → v\n '\\u03BF': 'o', // ο → o\n '\\u03C1': 'p', // ρ → p\n '\\u03C5': 'u', // υ → u\n '\\u03C7': 'x', // χ → x\n '\\u0391': 'a', // Α → a\n '\\u0392': 'b', // Β → b\n '\\u0395': 'e', // Ε → e\n '\\u0396': 'z', // Ζ → z\n '\\u0397': 'h', // Η → h\n '\\u0399': 'i', // Ι → i\n '\\u039A': 'k', // Κ → k\n '\\u039C': 'm', // Μ → m\n '\\u039D': 'n', // Ν → n\n '\\u039F': 'o', // Ο → o\n '\\u03A1': 'r', // Ρ → r\n '\\u03A4': 't', // Τ → t\n '\\u03A5': 'y', // Υ → y\n '\\u03A7': 'x', // Χ → x\n\n // Armenian → Latin\n '\\u0570': 'h', // հ → h\n '\\u0578': 'o', // օ → o (approximate)\n\n // Other common confusables\n '\\u2044': '/', // ⁄ fraction slash → /\n '\\u2215': '/', // ∕ division slash → /\n '\\u01A0': 'o', // Ơ → o\n '\\u0D20': 't', // ഠ → t (approximate)\n};\n\n// ─── Stage 3: Leet-speak map ──────────────────────────────────────────────────\nconst LEET_MAP: Record<string, string> = {\n '0': 'o',\n '1': 'i',\n '2': 'z',\n '3': 'e',\n '4': 'a',\n '5': 's',\n '6': 'g',\n '7': 't',\n '8': 'b',\n '9': 'g',\n '@': 'a',\n '$': 's',\n '!': 'i',\n '+': 't',\n '|': 'l',\n '(': 'c',\n};\n\n/**\n * Strips separator dots/dashes used to evade pattern matching.\n * \"p.o.r.n\" → \"porn\", \"f-u-c-k\" → \"fuck\"\n * Only strips if chars are separated by single punctuation (not spaces).\n */\nfunction stripSeparators(t: string): string {\n // Match: single letter/digit, followed by . or - or _, repeated\n return t.replace(/([a-z0-9])[.\\-_|\\\\\\/](?=[a-z0-9])/gi, '$1');\n}\n\n/**\n * Produce a \"skeleton\" string for pattern matching ONLY.\n *\n * Pipeline:\n * raw → NFKC → stripSeparators → homoglyph → lowercase → leet → strip non-alpha → collapse spaces\n *\n * @param t - Raw input string\n * @returns Normalized string safe for regex pattern matching\n */\nexport function toSkeleton(t: string): string {\n if (!t) return '';\n\n // Stage 1: Unicode NFKC normalization\n // Handles fullwidth chars, ligatures, math bold, superscripts, zero-width, bidi\n let s = t.normalize('NFKC');\n\n // Stage 2: Strip separator dots (p.o.r.n, f-u-c-k)\n s = stripSeparators(s);\n\n // Stage 3: Homoglyph substitution (Cyrillic, Greek, Armenian)\n s = s\n .split('')\n .map(c => HOMOGLYPH_MAP[c] ?? c)\n .join('');\n\n // Stage 4: Lowercase\n s = s.toLowerCase();\n\n // Stage 5: Leet-speak substitution\n s = s\n .split('')\n .map(c => LEET_MAP[c] ?? c)\n .join('');\n\n // Stage 6: Strip remaining non-alphanumeric (except spaces)\n s = s.replace(/[^a-z0-9\\s]/g, '');\n\n // Stage 7: Collapse whitespace\n return s.replace(/\\s+/g, ' ').trim();\n}\n\n/**\n * Lightweight normalization for structural checks (length, symbol ratio).\n * Does NOT apply leet/homoglyph maps — just trims and normalizes whitespace.\n * Preserves symbols so hasExcessiveSymbols() works correctly.\n */\nexport function toStructural(t: string): string {\n if (!t) return '';\n return t.normalize('NFKC').replace(/\\s+/g, ' ').trim();\n}\n","/**\n * profanity.ts\n *\n * Profanity detection. All patterns run against the skeleton (toSkeleton()),\n * meaning they automatically handle:\n * - Leet-speak: \"f4ck\", \"@ss\", \"sh!t\"\n * - Homoglyphs: \"fuсk\" (Cyrillic с), \"аss\" (Cyrillic а)\n * - Separator dots: \"f.u.c.k\", \"s-h-i-t\"\n * - Fullwidth: \"fuck\"\n * - Repeated chars: \"fuuuuck\", \"shhhhit\"\n *\n * Pattern design:\n * - Use \\b word boundaries so \"classic\" doesn't match \"ass\"\n * - Use + quantifiers to catch character stretching (\"fuuuuck\")\n * - Cover plurals and -er/-ing forms (shits, bitch, bitching)\n */\n\nimport { toSkeleton } from './normalize.js';\n\n// Each entry: [pattern, label] — label used for future i18n / logging\nconst PROFANITY_PATTERNS: RegExp[] = [\n // fuck — also matches 'fack' because leet '4' → 'a', so we allow u OR a in slot 2\n /\\bf+[ua]+c+k+(e[dr]|ing|s|er)?\\b/i,\n /\\bs+h+i+t+(s|te[dr]|ting)?\\b/i,\n /\\bb+i+t+c+h+(e[sd]|ing)?\\b/i,\n // ass — use (^|\\s|[^a-z]) instead of \\b so it catches \"@ss\" → \"ass\" at start of string\n /(?:^|(?<=[^a-z]))a+s{2,}(h+o+l+e+s?|e[sd]|ing)?\\b/i,\n /\\bc+u+n+t+s?\\b/i,\n // dick — exclude as a proper first name before a capitalized surname\n /\\bd+i+c+k+(s|ed|ing)?\\b(?! [A-Z])/i,\n /\\bp+r+i+c+k+s?\\b/i,\n /\\bb+a+s+t+a+r+d+s?\\b/i,\n /\\bw+h+o+r+e+s?\\b/i,\n /\\bf+a+g+(g+o+t+s?)?\\b/i,\n /\\bn+i+g+(g+e+r+s?|ga+s?)?\\b/i,\n /\\bfrick\\b/i,\n];\n\n/**\n * Returns true if the skeleton of `text` matches any profanity pattern.\n * Always check skeleton — never raw — so bypasses don't work.\n */\nexport function containsProfanity(text: string): boolean {\n const skeleton = toSkeleton(text);\n return PROFANITY_PATTERNS.some(p => p.test(skeleton));\n}\n\n/**\n * Returns the specific pattern that matched, or null.\n * Useful for debug logging (never expose to users directly).\n */\nexport function getMatchedProfanityPattern(text: string): RegExp | null {\n const skeleton = toSkeleton(text);\n return PROFANITY_PATTERNS.find(p => p.test(skeleton)) ?? null;\n}\n","/**\n * spam.ts\n *\n * Spam detection.\n *\n * CRITICAL DESIGN DECISION:\n * URL and domain patterns MUST run on the raw (or NFKC-only) text.\n * If you normalize first (stripping dots, slashes, colons), URLs become\n * undetectable. \"https://spam.com\" → after skeleton → \"httpsspamcom\" which\n * no URL regex can match.\n *\n * So: keyword spam runs on skeleton (catches leet evasion),\n * URL/domain spam runs on NFKC-normalized raw text only.\n */\n\nimport { toSkeleton } from './normalize.js';\n\n// ─── Keyword spam — run on skeleton ──────────────────────────────────────────\n// IMPORTANT: skeleton strips spaces to single space, so multi-word patterns\n// must use \\s+ not \\s* (skeleton never has zero spaces between real words).\n// \"free money\" → skeleton → \"free money\" (space preserved between words)\nconst KEYWORD_PATTERNS: RegExp[] = [\n /\\bviagra\\b/i,\n /\\bcialis\\b/i,\n /\\bcasino\\b/i,\n /\\bpoker\\b/i,\n /\\bfree\\s+money\\b/i, // skeleton preserves spaces between words\n /\\bbuy\\s+now\\b/i,\n /\\bclick\\s+here\\b/i,\n /\\blorem\\s+ipsum\\b/i,\n /\\bwork\\s+from\\s+home\\b/i,\n /\\bmake\\s+money\\b/i,\n /\\bget\\s+rich\\b/i,\n /\\bonlyfans\\b/i, // skeleton collapses \"only fans\" → \"only fans\" but domain is onlyfans\n];\n\n// ─── URL / domain patterns — run on raw (NFKC-normalized) text ───────────────\nconst URL_PATTERNS: RegExp[] = [\n // Full URLs\n /https?:\\/\\/[^\\s]{4,}/i,\n // Bare domains with common TLDs (not preceded by a letter — avoids \"version2.0\")\n /(?<![a-z])\\b[a-z0-9]([a-z0-9-]{1,61})\\.(com|net|org|io|xyz|ru|cn|co|info|biz|me|gg|app|dev|uk|de|fr)\\b/i,\n // IP addresses\n /\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/,\n];\n\n/**\n * Returns true if text contains spam keywords (checked on skeleton)\n * or URLs/domains (checked on NFKC-only normalized text).\n */\nexport function containsSpam(text: string): boolean {\n const skeleton = toSkeleton(text);\n const nfkc = text.normalize('NFKC'); // preserves punctuation — ! stays !, not 'i'\n\n // Keywords: run on NFKC (not skeleton) so trailing punctuation like \"money!\"\n // doesn't get leet-converted and break \\b word boundaries.\n // Leet-evaded keywords (v!agra) are caught by also testing the skeleton.\n if (KEYWORD_PATTERNS.some(p => p.test(nfkc) || p.test(skeleton))) return true;\n if (URL_PATTERNS.some(p => p.test(nfkc))) return true;\n\n return false;\n}","/**\n * gibberish.ts\n *\n * Gibberish / keyboard-mash detection with a configurable sensitivity scale.\n *\n * Why sensitivity levels?\n * \"Strict\" mode is needed for display names & usernames.\n * \"Loose\" mode prevents false positives on:\n * - Polish names: \"Krzysztof\", \"Szczepański\"\n * - Technical strings: \"kubectl\", \"nginx\", \"src\"\n * - Abbreviations: \"HVAC\", \"VLSI\"\n * - Short legitimate words like \"nth\", \"gym\", \"lynx\"\n *\n * Heuristics used (layered by sensitivity):\n * LOOSE: 7+ consonants in a row (obvious keyboard mash only)\n * NORMAL: 6+ consonants in a row OR vowel ratio < 10% on words ≥ 8 chars\n * STRICT: 5+ consonants in a row OR vowel ratio < 15% on words ≥ 6 chars\n * OR no vowels at all on words ≥ 4 chars\n *\n * All checks run on the skeleton so leet/homoglyphs are already resolved.\n */\n\nimport { toSkeleton } from './normalize.js';\nimport type { GibberishSensitivity } from '../types.js';\n\n// Legitimate words that trip consonant/vowel heuristics — never flag these.\nconst GIBBERISH_ALLOWLIST = new Set([\n 'rhythm', 'rhythms',\n 'krzysztof', 'szczepanski', 'grzegorz', 'przemek',\n 'strength', 'strengths',\n 'lymph', 'nymph', 'nymphs',\n 'glyph', 'glyphs', 'crypt', 'crypts',\n 'tryst', 'pygmy', 'synth', 'psych',\n]);\n\ninterface SensitivityConfig {\n consonantRun: number; // consecutive consonants that trigger flag\n vowelRatioMin: number; // minimum vowel ratio (below this = gibberish)\n vowelRatioWordLen: number; // word must be at least this long for ratio check\n noVowelWordLen: number; // word with zero vowels flagged if >= this length\n}\n\nconst CONFIGS: Record<GibberishSensitivity, SensitivityConfig> = {\n loose: {\n consonantRun: 7,\n vowelRatioMin: 0.05,\n vowelRatioWordLen: 12,\n noVowelWordLen: 6,\n },\n normal: {\n consonantRun: 6,\n vowelRatioMin: 0.10,\n vowelRatioWordLen: 8,\n noVowelWordLen: 5,\n },\n strict: {\n consonantRun: 5,\n vowelRatioMin: 0.15,\n vowelRatioWordLen: 6,\n noVowelWordLen: 4,\n },\n};\n\nconst CONSONANT_RUN_PATTERNS: Record<GibberishSensitivity, RegExp> = {\n loose: /[bcdfghjklmnpqrstvwxyz]{7,}/i,\n normal: /[bcdfghjklmnpqrstvwxyz]{6,}/i,\n strict: /[bcdfghjklmnpqrstvwxyz]{5,}/i,\n};\n\n/**\n * Returns true if the given word (already skeleton-normalized, no spaces)\n * looks like gibberish at the given sensitivity.\n */\nfunction isWordGibberish(word: string, sensitivity: GibberishSensitivity): boolean {\n if (word.length > 25) return true;\n\n // Never flag known legitimate words\n if (GIBBERISH_ALLOWLIST.has(word)) return false;\n\n const cfg = CONFIGS[sensitivity];\n\n if (CONSONANT_RUN_PATTERNS[sensitivity].test(word)) return true;\n\n if (word.length >= cfg.vowelRatioWordLen) {\n const vowels = (word.match(/[aeiou]/g) ?? []).length;\n if (vowels / word.length < cfg.vowelRatioMin) return true;\n }\n\n // Zero-vowel check — only apply to words NOT in allowlist (already checked above)\n if (word.length >= cfg.noVowelWordLen) {\n const vowels = (word.match(/[aeiou]/g) ?? []).length;\n if (vowels === 0) return true;\n }\n\n return false;\n}\n\n/**\n * Returns true if any word in the text looks like gibberish.\n *\n * Filters out very short words (< 4 chars) before applying heuristics\n * to prevent false positives on \"by\", \"mr\", \"st\", \"nth\", etc.\n */\nexport function isGibberish(\n text: string,\n sensitivity: GibberishSensitivity = 'normal'\n): boolean {\n const skeleton = toSkeleton(text);\n const words = skeleton.split(' ').filter(w => w.length >= 4);\n\n // If there are no words long enough to check, it's not gibberish by this heuristic\n if (words.length === 0) return false;\n\n return words.some(word => isWordGibberish(word, sensitivity));\n}\n\n/**\n * Returns true if the text contains 5+ of the same character consecutively.\n * e.g. \"aaaaaaa\", \"!!!!!!\", \"heeeeey\" (5 e's)\n * Runs on skeleton so leet chars are already resolved.\n */\nexport function hasRepeatingChars(text: string): boolean {\n return /(.)\\1{4,}/.test(toSkeleton(text));\n}\n","/**\n * structure.ts\n *\n * Structural quality checks. These run on the NFKC-normalized original text\n * (not the full skeleton), because they're measuring the *shape* of the input\n * (symbol density, letter presence) — not its semantic content.\n *\n * Running these on the skeleton would give wrong results because the skeleton\n * strips ALL symbols, making everything look clean structurally.\n */\n\nimport { toStructural } from './normalize.js';\n\n/**\n * Returns true if more than 40% of characters are symbols\n * (not letters, digits, or whitespace).\n *\n * Short strings (< 5 chars) are excluded — too noisy to judge.\n * e.g. \"!!??@@##\" → 100% symbols → flagged\n * \"Hello!!\" → 22% symbols → pass\n */\nexport function hasExcessiveSymbols(text: string): boolean {\n const s = toStructural(text);\n if (s.length < 5) return false;\n const symbols = (s.match(/[^a-zA-Z0-9\\s]/g) ?? []).length;\n return symbols / s.length > 0.40;\n}\n\n/**\n * Returns true if fewer than 20% of characters are letters AND\n * there are fewer than 3 total letter characters.\n *\n * This catches strings like \"123 456\", \"--- ---\", \"42\", \"!2!\"\n * but allows legitimate short inputs like \"QA\", \"IT\", \"Go\".\n *\n * Both conditions must be true to avoid false positives.\n */\nexport function hasLowAlphabetRatio(text: string): boolean {\n const s = toStructural(text);\n const letters = (s.match(/[a-zA-Z]/g) ?? []).length;\n\n // Short strings (≤ 5 chars): only flag if ZERO letters (e.g. \"123\", \"!!!\")\n // This allows \"QA\", \"IT\", \"Go\", \"v2\" to pass\n if (s.length <= 5) return letters === 0;\n\n // Medium+ strings: must have at least 3 letters\n if (letters < 3) return true;\n\n // Long strings (≥ 15 chars): flag if mostly non-letter\n if (s.length >= 15 && letters / s.length < 0.20) return true;\n\n return false;\n}\n\n/**\n * Detects repeated *content* words (length > 3).\n * Stop words (\"the\", \"and\", \"for\") are excluded to avoid false positives\n * in natural English sentences.\n *\n * Flags only when 2+ distinct content words appear more than once.\n * \"the cat sat on the mat\" → 0 repeated content words → pass\n * \"cat cat cat dog dog\" → \"cat\" repeated, \"dog\" repeated → flag\n */\nexport function hasRepeatedContentWords(text: string): boolean {\n const s = toStructural(text).toLowerCase();\n const words = s\n .replace(/[^a-z0-9\\s]/g, '')\n .split(/\\s+/)\n .filter(w => w.length > 3);\n\n const seen = new Set<string>();\n let repeatCount = 0;\n\n for (const word of words) {\n if (seen.has(word)) repeatCount++;\n seen.add(word);\n }\n\n return repeatCount >= 2;\n}\n\n// ─── Low-effort exact matches ─────────────────────────────────────────────────\n// Short exact-match blocklist for obvious filler inputs.\n// These are checked against the SKELETON so leet evasions are caught too.\nconst LOW_EFFORT_SET = new Set([\n 'test', 'testing', 'tester',\n 'demo', 'sample', 'trial',\n 'asdf', 'qwer', 'zxcv', 'qwerty', 'qwertyuiop',\n 'placeholder', 'foo', 'bar', 'baz', 'foobar',\n 'helloworld', // \"hello world\" after skeleton strip\n 'loremipsum', // same\n 'aaa', 'bbb', // caught by repeatingChars too, belt-and-suspenders\n 'xxx', 'yyy', 'zzz',\n 'na', 'none', 'null', 'undefined', 'nope', 'no', 'n/a',\n]);\n\n/**\n * Returns true if the skeleton of `text` exactly matches a known low-effort phrase.\n */\nexport function isLowEffortExact(skeletonText: string): boolean {\n // Remove spaces for compound checks (\"hello world\" → \"helloworld\")\n const noSpaces = skeletonText.replace(/\\s/g, '');\n return LOW_EFFORT_SET.has(skeletonText) || LOW_EFFORT_SET.has(noSpaces);\n}\n","/**\n * builder.ts\n *\n * Fluent builder API — the primary way developers use input-shield.\n *\n * Usage:\n * const validator = createValidator()\n * .field('Username')\n * .min(3).max(30)\n * .noProfanity()\n * .noGibberish({ sensitivity: 'strict' })\n * .noSpam()\n * .allow('nginx', 'kubectl');\n *\n * const result = validator.validate(userInput);\n * if (!result.isValid) console.error(result.reason, result.message);\n *\n * Design principles:\n * - Each check is opt-in via a chain method (not a giant options object)\n * - Checks run in declaration order (you control the priority)\n * - Returns typed FailReason, not just a boolean\n * - Allowlist bypasses ALL checks (brand names, abbreviations, etc.)\n * - Custom check via .custom() for domain-specific rules\n */\n\nimport { toSkeleton, toStructural } from '../core/normalize.js';\nimport { containsProfanity } from '../core/profanity.js';\nimport { containsSpam } from '../core/spam.js';\nimport { isGibberish, hasRepeatingChars } from '../core/gibberish.js';\nimport {\n hasExcessiveSymbols,\n hasLowAlphabetRatio,\n hasRepeatedContentWords,\n isLowEffortExact,\n} from '../core/structure.js';\nimport type { ValidationResult, FailReason, GibberishSensitivity } from '../types.js';\n\n// ─── Internal check type ──────────────────────────────────────────────────────\ntype CheckFn = (raw: string, skeleton: string) => CheckResult | null;\ntype CheckResult = { reason: FailReason; message: string };\n\n// ─── Builder class ────────────────────────────────────────────────────────────\nexport class InputShieldValidator {\n private _fieldName = 'Input';\n private _min = 2;\n private _max = 500;\n private _allowlist = new Set<string>();\n private _checks: CheckFn[] = [];\n private _spamCheck: CheckFn | null = null;\n\n // ─── Configuration ──────────────────────────────────────────────────────────\n\n /** Set the field label used in error messages */\n field(name: string): this {\n this._fieldName = name;\n return this;\n }\n\n /** Minimum character length (post-trim). Default: 2 */\n min(n: number): this {\n this._min = n;\n return this;\n }\n\n /** Maximum character length. Default: 500 */\n max(n: number): this {\n this._max = n;\n return this;\n }\n\n /**\n * Add strings that always pass validation, regardless of other checks.\n * Useful for brand names, tech terms, or known-good short strings.\n * .allow('nginx', 'kubectl', 'QA', 'IT')\n */\n allow(...words: string[]): this {\n words.forEach(w => this._allowlist.add(toSkeleton(w)));\n return this;\n }\n\n // ─── Check methods ──────────────────────────────────────────────────────────\n\n /** Block profanity, including leet-speak and homoglyph evasions */\n noProfanity(): this {\n this._checks.push((_raw, skeleton) =>\n containsProfanity(skeleton)\n ? { reason: 'profanity', message: 'contains inappropriate language.' }\n : null\n );\n return this;\n }\n\n /**\n * Block spam keywords and URLs.\n * Note: URL detection uses the raw string internally (not the skeleton),\n * so you don't need to worry about URLs being missed.\n */\n noSpam(): this {\n const spamFn: CheckFn = (raw) =>\n containsSpam(raw)\n ? { reason: 'spam', message: 'appears to contain spam or promotional content.' }\n : null;\n this._spamCheck = spamFn;\n this._checks.push(spamFn);\n return this;\n }\n\n /**\n * Block gibberish / keyboard mash.\n *\n * @param options.sensitivity\n * 'loose' — only catches extreme mashing (7+ consonants). Safe for names.\n * 'normal' — default. Catches most mashing while allowing tech words.\n * 'strict' — also catches low vowel-ratio words. Best for usernames.\n */\n noGibberish(options: { sensitivity?: GibberishSensitivity } = {}): this {\n const sensitivity = options.sensitivity ?? 'normal';\n this._checks.push((raw) => {\n if (hasRepeatingChars(raw) || isGibberish(raw, sensitivity)) {\n return { reason: 'gibberish', message: 'appears to be gibberish or keyboard mash.' };\n }\n return null;\n });\n return this;\n }\n\n /** Block inputs that are structurally low quality (too many symbols, too few letters) */\n noLowQuality(): this {\n this._checks.push((raw, skeleton) => {\n if (hasExcessiveSymbols(raw)) {\n return { reason: 'excessive_symbols', message: 'contains too many special characters.' };\n }\n if (hasLowAlphabetRatio(raw)) {\n return { reason: 'low_effort', message: 'must contain more letters.' };\n }\n if (isLowEffortExact(skeleton)) {\n return { reason: 'low_effort', message: 'appears to be a placeholder or filler value.' };\n }\n return null;\n });\n return this;\n }\n\n /** Block inputs that repeat content words excessively */\n noRepeatedWords(): this {\n this._checks.push((raw) =>\n hasRepeatedContentWords(raw)\n ? { reason: 'low_effort', message: 'contains too many repeated words.' }\n : null\n );\n return this;\n }\n\n /**\n * Add a custom validation rule.\n *\n * @param fn - Returns true if the input is INVALID (should be blocked)\n * @param reason - The FailReason code to return\n * @param message - Human-readable message (do not include fieldName, it's prepended)\n *\n * @example\n * .custom(t => t.includes('@'), 'custom', 'product names cannot contain @')\n */\n custom(fn: (text: string) => boolean, reason: FailReason, message: string): this {\n this._checks.push((raw) =>\n fn(raw) ? { reason, message } : null\n );\n return this;\n }\n\n // ─── Validation ─────────────────────────────────────────────────────────────\n\n validate(text: string): ValidationResult {\n const raw = (text ?? '').trim();\n const skeleton = toSkeleton(raw);\n const f = this._fieldName;\n\n // 1. Allowlist bypass\n if (this._allowlist.has(skeleton)) {\n return { isValid: true };\n }\n\n // 2. Empty\n if (!raw) {\n return { isValid: false, reason: 'empty', message: `${f} cannot be empty.` };\n }\n\n // 3. Length\n const displayLength = toStructural(raw).length;\n if (displayLength < this._min) {\n return { isValid: false, reason: 'too_short', message: `${f} must be at least ${this._min} characters.` };\n }\n if (displayLength > this._max) {\n return { isValid: false, reason: 'too_long', message: `${f} must be no more than ${this._max} characters.` };\n }\n\n // 4. Run spam check FIRST before other checks (priority override)\n // Reason: a URL like \"https://spam.com\" would otherwise get caught\n // by the gibberish heuristic (consonant clusters in domain names)\n // and return reason:'gibberish' instead of reason:'spam'.\n const spamCheck = this._checks.find(c => c === this._spamCheck);\n if (spamCheck) {\n const result = spamCheck(raw, skeleton);\n if (result) return { isValid: false, reason: result.reason, message: `${f} ${result.message}` };\n }\n\n // 5. Run remaining checks in declaration order\n for (const check of this._checks) {\n if (check === this._spamCheck) continue; // already ran above\n const result = check(raw, skeleton);\n if (result) {\n return { isValid: false, reason: result.reason, message: `${f} ${result.message}` };\n }\n }\n\n return { isValid: true };\n }\n\n /**\n * Validate and throw if invalid. Useful in form libraries / Zod .superRefine().\n * @throws Error with the validation message\n */\n validateOrThrow(text: string): void {\n const result = this.validate(text);\n if (!result.isValid) throw new Error(result.message);\n }\n}\n\n/**\n * Create a new fluent validator.\n * No `new` keyword needed.\n *\n * @example\n * const v = createValidator().field('Username').min(3).noProfanity().noGibberish();\n */\nexport function createValidator(): InputShieldValidator {\n return new InputShieldValidator();\n}\n"]}