@marslanmustafa/input-shield 0.1.2 → 0.1.4

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 CHANGED
@@ -5,62 +5,135 @@
5
5
  [![license](https://img.shields.io/npm/l/@marslanmustafa/input-shield)](LICENSE)
6
6
  [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](package.json)
7
7
 
8
- > **One install. No config. Clean inputs.**
9
- >
10
- > Profanity · Spam · Gibberish · Leet-speak · Homoglyphs — all in one zero-dependency TypeScript package.
8
+ > One install. No config. Clean inputs.
9
+ > Profanity, spam, gibberish, and homoglyph detection in a single TypeScript-native zero-dependency package.
11
10
 
12
11
  ---
13
12
 
14
- ## Why @marslanmustafa/input-shield?
13
+ ## Why input-shield?
15
14
 
16
- | Problem | Old way (3+ packages) | @marslanmustafa/input-shield |
17
- |---|---|---|
18
- | Block profanity | `npm i obscenity` | built in |
19
- | Catch `f.u.c.k` / `f@ck` / `fuck` | Manual regex + leet map | ✅ 3-stage normalization |
20
- | Catch Cyrillic `аss` (homoglyph bypass) | Nobody does this | ✅ NFKC + homoglyph map |
21
- | Block spam URLs | `npm i validator` | built in |
22
- | Detect keyboard mash | Hand-rolled heuristics | `loose/normal/strict` scale |
23
- | Zod integration | 20 lines of glue code | ✅ `import from '@marslanmustafa/input-shield/zod'` |
24
- | TypeScript types | Often partial | first-class, discriminated union |
25
- | Tree-shakeable | Rarely | ✅ every function importable alone |
15
+ Most profanity filters use a simple word list. They miss:
16
+
17
+ - **Leet-speak**: `Fr33 m0ney` normalized `free money` caught
18
+ - **Homoglyph attacks**: Greek `о` instead of English `o` normalized caught
19
+ - **Unicode evasion**: `fuck` (fullwidth) normalized caught
20
+ - **Split-tag HTML**: `<b>f</b>uck` stripped normalized caught
21
+ - **Gibberish**: `asdfghjkl` passes every regex filter not this one
22
+
23
+ input-shield runs a **full normalization pipeline** before any check, so evasion techniques don't work.
24
+
25
+ ```
26
+ Raw Input
27
+
28
+ Normalization (leet-speak, homoglyphs, unicode, fullwidth)
29
+
30
+ Skeleton (removes repeated chars, punctuation tricks)
31
+
32
+ Security Checks (profanity → spam → gibberish → structure)
33
+
34
+ ValidationResult { isValid, reason, message }
35
+ ```
26
36
 
27
37
  ---
28
38
 
29
- ## Install
39
+ ## Installation
30
40
 
31
41
  ```bash
32
- npm install @marslanmustafa/@marslanmustafa/input-shield
33
- pnpm add @marslanmustafa/@marslanmustafa/input-shield
34
- yarn add @marslanmustafa/@marslanmustafa/input-shield
42
+ # npm
43
+ npm install @marslanmustafa/input-shield
44
+
45
+ # pnpm
46
+ pnpm add @marslanmustafa/input-shield
47
+
48
+ # yarn
49
+ yarn add @marslanmustafa/input-shield
50
+
51
+ # bun
52
+ bun add @marslanmustafa/input-shield
35
53
  ```
36
54
 
55
+ > **Node.js >= 18** required. Zero runtime dependencies.
56
+
37
57
  ---
38
58
 
39
- ## Quick start
59
+ ## Quick Start
40
60
 
41
- ```typescript
61
+ ```ts
42
62
  import { createValidator } from '@marslanmustafa/input-shield';
43
63
 
44
- const usernameValidator = createValidator()
64
+ const validator = createValidator()
45
65
  .field('Username')
46
- .min(3).max(30)
66
+ .min(3)
67
+ .max(30)
47
68
  .noProfanity()
48
- .noGibberish({ sensitivity: 'strict' })
49
- .noSpam();
69
+ .noGibberish();
50
70
 
51
- const result = usernameValidator.validate(userInput);
71
+ const result = validator.validate('asdfghjkl');
52
72
 
53
73
  if (!result.isValid) {
54
- console.log(result.reason); // 'profanity' | 'gibberish' | 'spam' | ...
55
- console.log(result.message); // "Username: contains inappropriate language."
74
+ console.log(result.message); // "Username appears to be gibberish."
75
+ console.log(result.reason); // "GIBBERISH"
56
76
  }
57
77
  ```
58
78
 
59
79
  ---
60
80
 
61
- ## Presets (zero-config)
81
+ ## Fluent API
82
+
83
+ Chain as many rules as you need. Every method returns `this` so chains are fully composable.
84
+
85
+ ```ts
86
+ import { createValidator } from '@marslanmustafa/input-shield';
87
+
88
+ // Username
89
+ const username = createValidator()
90
+ .field('Username')
91
+ .min(3)
92
+ .max(30)
93
+ .noProfanity()
94
+ .noGibberish({ sensitivity: 'strict' });
95
+
96
+ // Bio
97
+ const bio = createValidator()
98
+ .field('Bio')
99
+ .min(10)
100
+ .max(300)
101
+ .noProfanity()
102
+ .noSpam()
103
+ .noGibberish();
104
+
105
+ // Search query
106
+ const search = createValidator()
107
+ .field('Search')
108
+ .min(1)
109
+ .max(100)
110
+ .noSpam();
111
+
112
+ // Validate
113
+ const result = bio.validate('Buy cheap pills now!!! Click here!!!');
114
+ // { isValid: false, reason: 'SPAM', message: 'Bio appears to contain spam.' }
115
+ ```
116
+
117
+ ### Available chain methods
118
+
119
+ | Method | Description |
120
+ |---|---|
121
+ | `.field(name)` | Sets the field name used in error messages |
122
+ | `.min(n)` | Minimum character length |
123
+ | `.max(n)` | Maximum character length |
124
+ | `.noProfanity()` | Detects profanity including leet-speak and homoglyphs |
125
+ | `.noSpam()` | Detects spam patterns, excessive URLs, repeated phrases |
126
+ | `.noGibberish(options?)` | Detects keyboard mash and random character sequences |
127
+ | `.noExcessiveSymbols()` | Rejects inputs with too many special characters |
128
+ | `.validate(value)` | Runs all checks and returns `ValidationResult` |
129
+
130
+ ---
131
+
132
+ ## Presets
133
+
134
+ Ready-to-use validators for common fields. No configuration needed.
62
135
 
63
- ```typescript
136
+ ```ts
64
137
  import {
65
138
  validateUsername,
66
139
  validateBio,
@@ -69,170 +142,276 @@ import {
69
142
  validateSearchQuery,
70
143
  } from '@marslanmustafa/input-shield';
71
144
 
72
- validateUsername('alice_dev'); // { isValid: true }
73
- validateUsername('f4ck3r'); // { isValid: false, reason: 'profanity', message: '...' }
74
- validateBio('Software engineer from Lahore'); // { isValid: true }
75
- validateShortText('test'); // { isValid: false, reason: 'low_effort', message: '...' }
76
- ```
145
+ validateUsername('h4ck3r_dude');
146
+ // { isValid: true }
77
147
 
78
- ---
148
+ validateUsername('ааааааааа'); // Cyrillic homoglyphs
149
+ // { isValid: false, reason: 'PROFANITY', message: '...' }
79
150
 
80
- ## Integrations
151
+ validateBio('Buy cheap Viagra now! Click here for free money!!!');
152
+ // { isValid: false, reason: 'SPAM', message: '...' }
81
153
 
82
- - 📧 [Nodemailer — validate email content before sending](./NODEMAILER.md)
83
- ## Fluent builder API
154
+ validateShortText('asdfasdfasdf');
155
+ // { isValid: false, reason: 'GIBBERISH', message: '...' }
84
156
 
85
- ```typescript
86
- import { createValidator } from '@marslanmustafa/input-shield';
157
+ validateLongText('Hello, this is a proper comment about the topic.');
158
+ // { isValid: true }
87
159
 
88
- createValidator()
89
- .field('Product Name') // shown in error messages
90
- .min(2).max(60) // length bounds
91
- .noProfanity() // catches leet, homoglyphs, dots (f.u.c.k), fullwidth (fuck)
92
- .noSpam() // keywords + URLs (on raw text — not destroyed by normalization)
93
- .noGibberish({ // sensitivity: 'loose' | 'normal' | 'strict'
94
- sensitivity: 'normal', // default — good for most fields
95
- })
96
- .noLowQuality() // exact matches (test, asdf), excessive symbols, low letter ratio
97
- .noRepeatedWords() // catches "cat cat cat" (ignores stop words)
98
- .allow('nginx', 'kubectl') // allowlist bypasses ALL checks (brand names, tech terms)
99
- .custom( // custom rule — return true to BLOCK
100
- t => t.startsWith('@'),
101
- 'custom',
102
- 'names cannot start with @'
103
- )
104
- .validate(text); // → ValidationResult
160
+ validateSearchQuery('!!!!!!!!!!!!');
161
+ // { isValid: false, reason: 'EXCESSIVE_SYMBOLS', message: '...' }
105
162
  ```
106
163
 
164
+ | Preset | Min | Max | Checks |
165
+ |---|---|---|---|
166
+ | `validateUsername` | 3 | 30 | profanity, gibberish (strict) |
167
+ | `validateBio` | 10 | 300 | profanity, spam |
168
+ | `validateShortText` | 2 | 100 | profanity, spam, gibberish |
169
+ | `validateLongText` | 5 | 2000 | profanity, spam, gibberish |
170
+ | `validateSearchQuery` | 1 | 100 | spam, symbols |
171
+
107
172
  ---
108
173
 
109
- ## Zod integration
174
+ ## Zod Integration
175
+
176
+ Install Zod separately (`zod >= 3.0.0` is a peer dependency):
110
177
 
111
178
  ```bash
112
- # zod is a peer dependency — install it separately
113
- npm install zod @marslanmustafa/input-shield
179
+ npm install zod
114
180
  ```
115
181
 
116
- ```typescript
182
+ Import from the `/zod` subpath to keep Zod out of your main bundle if unused:
183
+
184
+ ```ts
117
185
  import { z } from 'zod';
118
- import { shieldString, zodUsername, zodBio } from '@marslanmustafa/input-shield/zod';
186
+ import { shieldString, zodUsername, zodBio, zodShortText, zodLongText } from '@marslanmustafa/input-shield/zod';
119
187
 
120
- // Preset schemas
188
+ // Custom validator with full fluent chain
121
189
  const schema = z.object({
122
- username: zodUsername(),
123
- bio: zodBio(),
190
+ username: shieldString(v => v.field('Username').min(3).max(20).noProfanity().noGibberish()),
191
+ bio: shieldString(v => v.field('Bio').min(10).max(300).noProfanity().noSpam()),
124
192
  });
125
193
 
126
- // Custom configured schema
127
- const customSchema = z.object({
128
- productName: shieldString(v =>
129
- v.field('Product Name').min(2).max(60).noProfanity().noSpam()
130
- ),
194
+ // Or use preset Zod helpers
195
+ const schema = z.object({
196
+ username: zodUsername(),
197
+ bio: zodBio(),
198
+ title: zodShortText('Title'),
199
+ body: zodLongText('Body'),
131
200
  });
132
201
 
133
- // Usage with react-hook-form + zod resolverzero extra code
202
+ // Works with React Hook Form, tRPC, Next.js API routes anywhere Zod is used
203
+ const parsed = schema.safeParse({ username: 'cl3an_user', bio: 'Hello world!' });
134
204
  ```
135
205
 
136
206
  ---
137
207
 
138
- ## How the normalization pipeline works
139
-
140
- This is the core innovation. Input is processed through 3 stages before any check runs:
141
-
142
- ```
143
- Input: "P.0.r.n" or "fuck" or "аss" (Cyrillic а) or "f@ck"
144
-
145
- ▼ Stage 1: Unicode NFKC
146
- Collapses fullwidth, math-bold, ligatures, zero-width chars
147
- "fuck" → "fuck" | "𝐅𝐔𝐂𝐊" → "FUCK"
148
-
149
- Stage 2: Separator stripping
150
- Removes dots/dashes between single chars
151
- "P.0.r.n" → "P0rn" | "f-u-c-k" → "fuck"
152
-
153
- ▼ Stage 3: Homoglyph map (Cyrillic/Greek/Armenian → Latin)
154
- "аss" (Cyrillic а U+0430) "ass"
155
- "ρorn" (Greek ρ) → "porn"
156
-
157
- Stage 4: Leet-speak substitution
158
- "0" → "o", "@" → "a", "$" → "s", "!" → "i" …
159
- "f@ck" "fack" | "@ss" → "ass"
160
-
161
- Skeleton: "fuck" / "ass" / "porn"
162
- Pattern matching runs here ───────────────► BLOCKED
163
- Error messages reference ORIGINAL input ──► "f@ck"
208
+ ## Email / Nodemailer Integration
209
+
210
+ Import from the `/email` subpath:
211
+
212
+ ```ts
213
+ import { validateMailContent, stripHtml } from '@marslanmustafa/input-shield/email';
214
+ ```
215
+
216
+ ### Validate before sending
217
+
218
+ ```ts
219
+ import nodemailer from 'nodemailer';
220
+ import { validateMailContent } from '@marslanmustafa/input-shield/email';
221
+
222
+ const mail = {
223
+ subject: 'Your order is confirmed',
224
+ html: '<p>Thanks for your purchase! <a href="https://yoursite.com">View order</a></p>',
225
+ };
226
+
227
+ const result = validateMailContent(mail);
228
+
229
+ if (!result.isValid) {
230
+ // result.field → 'subject' | 'text' | 'html'
231
+ // result.reason 'PROFANITY' | 'SPAM' | ...
232
+ // result.message human-readable string
233
+ throw new Error(`Mail rejected on field "${result.field}": ${result.message}`);
234
+ }
235
+
236
+ await transporter.sendMail({ to: '...', ...mail });
237
+ ```
238
+
239
+ ### What it catches in HTML emails
240
+
241
+ ```ts
242
+ import { stripHtml } from '@marslanmustafa/input-shield/email';
243
+
244
+ // Split-tag evasion
245
+ stripHtml('<b>f</b><b>uck</b>'); // → "f uck" → skeleton → "fuck"
246
+
247
+ // Decimal entity encoding
248
+ stripHtml('&#102;&#117;&#99;&#107;'); // → "fuck"
249
+
250
+ // Spam URLs in href
251
+ stripHtml('<a href="https://spam.com">click here</a>'); // → includes URL text
252
+
253
+ // CSS background trackers
254
+ stripHtml('<div style="background:url(https://tracker.spam.com/px)">hi</div>');
255
+ // → includes tracker URL for spam check
256
+ ```
257
+
258
+ ### Custom validator for email
259
+
260
+ ```ts
261
+ import { createValidator } from '@marslanmustafa/input-shield';
262
+ import { validateMailContent } from '@marslanmustafa/input-shield/email';
263
+
264
+ const strictValidator = createValidator()
265
+ .field('Email content')
266
+ .min(1)
267
+ .max(10000)
268
+ .noProfanity()
269
+ .noSpam();
270
+
271
+ const result = validateMailContent(
272
+ { subject: 'Hello', html: '<p>Content here</p>' },
273
+ strictValidator
274
+ );
164
275
  ```
165
276
 
166
277
  ---
167
278
 
168
- ## API Reference
279
+ ## Core Primitives (Tree-Shakeable)
169
280
 
170
- ### `createValidator(): InputShieldValidator`
171
- Returns a new fluent builder instance.
281
+ Use individual functions directly if you need fine-grained control:
172
282
 
173
- ### Builder methods
283
+ ```ts
284
+ import { toSkeleton, toStructural } from '@marslanmustafa/input-shield';
285
+ import { containsProfanity } from '@marslanmustafa/input-shield';
286
+ import { containsSpam } from '@marslanmustafa/input-shield';
287
+ import { isGibberish, hasRepeatingChars } from '@marslanmustafa/input-shield';
288
+ import { hasExcessiveSymbols, hasLowAlphabetRatio } from '@marslanmustafa/input-shield';
174
289
 
175
- | Method | Description |
176
- |---|---|
177
- | `.field(name)` | Set field label for error messages |
178
- | `.min(n)` | Minimum length. Default: 2 |
179
- | `.max(n)` | Maximum length. Default: 500 |
180
- | `.allow(...words)` | Allowlist — bypasses all checks |
181
- | `.noProfanity()` | Block profanity (all evasions caught) |
182
- | `.noSpam()` | Block spam keywords and URLs |
183
- | `.noGibberish(opts?)` | Block keyboard mash. `opts.sensitivity`: `'loose'` / `'normal'` / `'strict'` |
184
- | `.noLowQuality()` | Block exact low-effort matches, excessive symbols, low letter ratio |
185
- | `.noRepeatedWords()` | Block inputs with many repeated content words |
186
- | `.custom(fn, reason, msg)` | Custom rule. `fn(text) => true` to BLOCK |
187
- | `.validate(text)` | Run all checks. Returns `ValidationResult` |
188
- | `.validateOrThrow(text)` | Throws on invalid input. Useful in Zod `.superRefine()` |
189
-
190
- ### `ValidationResult` (discriminated union)
191
-
192
- ```typescript
290
+ // Normalization
291
+ toSkeleton('Fr33 m0ney!!!'); // → "free money"
292
+ toStructural('fuck'); // "fuck"
293
+
294
+ // Individual checks
295
+ containsProfanity('h3ll yeah'); // true
296
+ containsSpam('Buy now! Click here! Free!!!'); // true
297
+ isGibberish('asdfghjkl'); // true
298
+ hasRepeatingChars('heeeeello'); // true
299
+ hasExcessiveSymbols('!!!###$$$'); // true
300
+ ```
301
+
302
+ ---
303
+
304
+ ## TypeScript Types
305
+
306
+ ```ts
307
+ import type {
308
+ ValidationResult,
309
+ FailReason,
310
+ GibberishSensitivity,
311
+ ValidationOptions,
312
+ } from '@marslanmustafa/input-shield';
313
+
314
+ // ValidationResult
193
315
  type ValidationResult =
194
316
  | { isValid: true }
195
317
  | { isValid: false; reason: FailReason; message: string };
196
318
 
319
+ // FailReason
197
320
  type FailReason =
198
- | 'empty' | 'too_short' | 'too_long'
199
- | 'profanity' | 'spam'
200
- | 'gibberish' | 'low_effort' | 'repeating_chars' | 'excessive_symbols'
201
- | 'homoglyph_attack' | 'custom';
321
+ | 'TOO_SHORT'
322
+ | 'TOO_LONG'
323
+ | 'PROFANITY'
324
+ | 'SPAM'
325
+ | 'GIBBERISH'
326
+ | 'EXCESSIVE_SYMBOLS'
327
+ | 'LOW_ALPHABET_RATIO'
328
+ | 'REPEATED_CONTENT'
329
+ | 'LOW_EFFORT';
330
+
331
+ // GibberishSensitivity
332
+ type GibberishSensitivity = 'strict' | 'normal' | 'loose';
202
333
  ```
203
334
 
204
- ### Sensitivity scale
335
+ ---
205
336
 
206
- | Sensitivity | Consonant run | Vowel ratio check | Best for |
207
- |---|---|---|---|
208
- | `'loose'` | 7+ in a row | ≥ 12 chars, < 5% vowels | Bio, non-English names |
209
- | `'normal'` | 6+ in a row | ≥ 8 chars, < 10% vowels | Most fields |
210
- | `'strict'` | 5+ in a row | ≥ 6 chars, < 15% vowels | Usernames, display names |
337
+ ## Real-World Examples
211
338
 
212
- ---
339
+ ### Next.js API Route
340
+
341
+ ```ts
342
+ import { validateUsername, validateBio } from '@marslanmustafa/input-shield';
343
+
344
+ export async function POST(req: Request) {
345
+ const { username, bio } = await req.json();
346
+
347
+ const usernameResult = validateUsername(username);
348
+ if (!usernameResult.isValid) {
349
+ return Response.json({ error: usernameResult.message }, { status: 400 });
350
+ }
351
+
352
+ const bioResult = validateBio(bio);
353
+ if (!bioResult.isValid) {
354
+ return Response.json({ error: bioResult.message }, { status: 400 });
355
+ }
356
+
357
+ // safe to write to DB
358
+ }
359
+ ```
360
+
361
+ ### tRPC Procedure
362
+
363
+ ```ts
364
+ import { z } from 'zod';
365
+ import { zodUsername, zodBio } from '@marslanmustafa/input-shield/zod';
366
+
367
+ export const updateProfile = publicProcedure
368
+ .input(z.object({
369
+ username: zodUsername(),
370
+ bio: zodBio(),
371
+ }))
372
+ .mutation(async ({ input }) => {
373
+ // input is fully validated and typed
374
+ await db.user.update({ data: input });
375
+ });
376
+ ```
213
377
 
214
- ## Tree-shaking
378
+ ### Express Middleware
215
379
 
216
- Every module is independently importable:
380
+ ```ts
381
+ import { createValidator } from '@marslanmustafa/input-shield';
217
382
 
218
- ```typescript
219
- import { toSkeleton } from '@marslanmustafa/input-shield'; // normalization only
220
- import { containsProfanity } from '@marslanmustafa/input-shield'; // profanity only
221
- import { isGibberish } from '@marslanmustafa/input-shield'; // gibberish only
383
+ const commentValidator = createValidator()
384
+ .field('Comment')
385
+ .min(5)
386
+ .max(500)
387
+ .noProfanity()
388
+ .noSpam()
389
+ .noGibberish();
390
+
391
+ app.post('/comments', (req, res) => {
392
+ const result = commentValidator.validate(req.body.comment);
393
+ if (!result.isValid) {
394
+ return res.status(400).json({ error: result.message, reason: result.reason });
395
+ }
396
+ // save comment
397
+ });
222
398
  ```
223
399
 
224
400
  ---
225
401
 
226
- ## Bundle size
402
+ ## Contributing
227
403
 
228
- | Import | Size (minzipped) |
229
- |---|---|
230
- | `createValidator` (full builder) | ~4 KB |
231
- | `containsProfanity` only | ~1.5 KB |
232
- | `@marslanmustafa/input-shield/zod` | +1 KB (zod external) |
404
+ Issues and PRs are welcome. Please open an issue first for major changes.
405
+
406
+ ```bash
407
+ git clone https://github.com/marslanmustafa/input-shield
408
+ cd input-shield
409
+ npm install
410
+ npm run test:watch
411
+ ```
233
412
 
234
413
  ---
235
414
 
236
415
  ## License
237
416
 
238
- MIT © [Muhammad Arslan](https://marslanmustafa.com)
417
+ MIT © [Muhammad Arslan Mustafa](https://marslanmustafa.com)
@@ -1,4 +1,4 @@
1
- import { createValidator } from './chunk-ADGSP522.mjs';
1
+ import { createValidator } from './chunk-SX25LIYU.mjs';
2
2
 
3
3
  // src/validators/presets.ts
4
4
  function validateUsername(text) {
@@ -18,5 +18,5 @@ function validateSearchQuery(text) {
18
18
  }
19
19
 
20
20
  export { validateBio, validateLongText, validateSearchQuery, validateShortText, validateUsername };
21
- //# sourceMappingURL=chunk-WACGX73I.mjs.map
22
- //# sourceMappingURL=chunk-WACGX73I.mjs.map
21
+ //# sourceMappingURL=chunk-ELNY5LCC.mjs.map
22
+ //# sourceMappingURL=chunk-ELNY5LCC.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/validators/presets.ts"],"names":[],"mappings":";;;AAsBO,SAAS,iBAAiB,IAAA,EAAgC;AAC/D,EAAA,OAAO,eAAA,GACJ,KAAA,CAAM,UAAU,EAChB,GAAA,CAAI,CAAC,CAAA,CACL,GAAA,CAAI,EAAE,CAAA,CACN,aAAY,CACZ,WAAA,CAAY,EAAE,WAAA,EAAa,QAAA,EAAU,CAAA,CACrC,YAAA,EAAa,CACb,QAAA,CAAS,IAAI,CAAA;AAClB;AAMO,SAAS,iBAAA,CAAkB,IAAA,EAAc,SAAA,GAAY,MAAA,EAA0B;AACpF,EAAA,OAAO,eAAA,EAAgB,CACpB,KAAA,CAAM,SAAS,CAAA,CACf,IAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAG,CAAA,CACP,WAAA,GACA,WAAA,CAAY,EAAE,WAAA,EAAa,QAAA,EAAU,CAAA,CACrC,QAAO,CACP,YAAA,EAAa,CACb,QAAA,CAAS,IAAI,CAAA;AAClB;AAOO,SAAS,YAAY,IAAA,EAAgC;AAC1D,EAAA,OAAO,eAAA,GACJ,KAAA,CAAM,KAAK,EACX,GAAA,CAAI,EAAE,CAAA,CACN,GAAA,CAAI,GAAG,CAAA,CACP,aAAY,CACZ,MAAA,GACA,WAAA,CAAY,EAAE,aAAa,OAAA,EAAS,CAAA,CACpC,QAAA,CAAS,IAAI,CAAA;AAClB;AAOO,SAAS,gBAAA,CAAiB,IAAA,EAAc,SAAA,GAAY,SAAA,EAA6B;AACtF,EAAA,OAAO,iBAAgB,CACpB,KAAA,CAAM,SAAS,CAAA,CACf,IAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAI,EACR,WAAA,EAAY,CACZ,MAAA,EAAO,CACP,SAAS,IAAI,CAAA;AAClB;AAOO,SAAS,oBAAoB,IAAA,EAAgC;AAClE,EAAA,OAAO,eAAA,EAAgB,CACpB,KAAA,CAAM,cAAc,EACpB,GAAA,CAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAG,CAAA,CACP,MAAA,EAAO,CACP,SAAS,IAAI,CAAA;AAClB","file":"chunk-WACGX73I.mjs","sourcesContent":["/**\n * presets.ts\n *\n * Ready-to-use validators for the most common field types.\n * Zero config — import and call.\n *\n * All presets are pre-configured InputShieldValidator instances.\n * You can also use them as a starting point and extend with .custom():\n *\n * import { usernameValidator } from 'input-shield/presets';\n * // They're factories, so each call gives a fresh instance:\n * const myValidator = usernameValidator().custom(t => t === 'admin', 'custom', 'reserved name');\n */\n\nimport { createValidator } from './builder.js';\nimport type { ValidationResult } from '../types.js';\n\n/**\n * Username / display name.\n * 3–30 chars, no profanity, strict gibberish detection, no spam.\n * Repeated words allowed (e.g. \"John John\" as a nickname).\n */\nexport function validateUsername(text: string): ValidationResult {\n return createValidator()\n .field('Username')\n .min(3)\n .max(30)\n .noProfanity()\n .noGibberish({ sensitivity: 'strict' })\n .noLowQuality()\n .validate(text);\n}\n\n/**\n * Short text / name fields (product name, company name, form title).\n * 2–100 chars, no profanity, normal gibberish, no spam.\n */\nexport function validateShortText(text: string, fieldName = 'Name'): ValidationResult {\n return createValidator()\n .field(fieldName)\n .min(2)\n .max(100)\n .noProfanity()\n .noGibberish({ sensitivity: 'normal' })\n .noSpam()\n .noLowQuality()\n .validate(text);\n}\n\n/**\n * Bio / description / about me.\n * 10–300 chars, no profanity, no spam, loose gibberish (allows natural language).\n * Repeated words NOT flagged (natural in prose).\n */\nexport function validateBio(text: string): ValidationResult {\n return createValidator()\n .field('Bio')\n .min(10)\n .max(300)\n .noProfanity()\n .noSpam()\n .noGibberish({ sensitivity: 'loose' })\n .validate(text);\n}\n\n/**\n * Long-form text (comment, review, feedback).\n * 5–2000 chars, no profanity, no spam. No gibberish check\n * (long text can contain intentional fragments, code, etc.)\n */\nexport function validateLongText(text: string, fieldName = 'Message'): ValidationResult {\n return createValidator()\n .field(fieldName)\n .min(5)\n .max(2000)\n .noProfanity()\n .noSpam()\n .validate(text);\n}\n\n/**\n * Search query input.\n * 1–200 chars, no spam URLs (but allows short/fragmentary text).\n * Does NOT flag gibberish (code snippets, SKUs, part numbers are valid queries).\n */\nexport function validateSearchQuery(text: string): ValidationResult {\n return createValidator()\n .field('Search query')\n .min(1)\n .max(200)\n .noSpam()\n .validate(text);\n}\n"]}
1
+ {"version":3,"sources":["../src/validators/presets.ts"],"names":[],"mappings":";;;AAsBO,SAAS,iBAAiB,IAAA,EAAgC;AAC/D,EAAA,OAAO,eAAA,GACJ,KAAA,CAAM,UAAU,EAChB,GAAA,CAAI,CAAC,CAAA,CACL,GAAA,CAAI,EAAE,CAAA,CACN,aAAY,CACZ,WAAA,CAAY,EAAE,WAAA,EAAa,QAAA,EAAU,CAAA,CACrC,YAAA,EAAa,CACb,QAAA,CAAS,IAAI,CAAA;AAClB;AAMO,SAAS,iBAAA,CAAkB,IAAA,EAAc,SAAA,GAAY,MAAA,EAA0B;AACpF,EAAA,OAAO,eAAA,EAAgB,CACpB,KAAA,CAAM,SAAS,CAAA,CACf,IAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAG,CAAA,CACP,WAAA,GACA,WAAA,CAAY,EAAE,WAAA,EAAa,QAAA,EAAU,CAAA,CACrC,QAAO,CACP,YAAA,EAAa,CACb,QAAA,CAAS,IAAI,CAAA;AAClB;AAOO,SAAS,YAAY,IAAA,EAAgC;AAC1D,EAAA,OAAO,eAAA,GACJ,KAAA,CAAM,KAAK,EACX,GAAA,CAAI,EAAE,CAAA,CACN,GAAA,CAAI,GAAG,CAAA,CACP,aAAY,CACZ,MAAA,GACA,WAAA,CAAY,EAAE,aAAa,OAAA,EAAS,CAAA,CACpC,QAAA,CAAS,IAAI,CAAA;AAClB;AAOO,SAAS,gBAAA,CAAiB,IAAA,EAAc,SAAA,GAAY,SAAA,EAA6B;AACtF,EAAA,OAAO,iBAAgB,CACpB,KAAA,CAAM,SAAS,CAAA,CACf,IAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAI,EACR,WAAA,EAAY,CACZ,MAAA,EAAO,CACP,SAAS,IAAI,CAAA;AAClB;AAOO,SAAS,oBAAoB,IAAA,EAAgC;AAClE,EAAA,OAAO,eAAA,EAAgB,CACpB,KAAA,CAAM,cAAc,EACpB,GAAA,CAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAG,CAAA,CACP,MAAA,EAAO,CACP,SAAS,IAAI,CAAA;AAClB","file":"chunk-ELNY5LCC.mjs","sourcesContent":["/**\n * presets.ts\n *\n * Ready-to-use validators for the most common field types.\n * Zero config — import and call.\n *\n * All presets are pre-configured InputShieldValidator instances.\n * You can also use them as a starting point and extend with .custom():\n *\n * import { usernameValidator } from 'input-shield/presets';\n * // They're factories, so each call gives a fresh instance:\n * const myValidator = usernameValidator().custom(t => t === 'admin', 'custom', 'reserved name');\n */\n\nimport { createValidator } from './builder.js';\nimport type { ValidationResult } from '../types.js';\n\n/**\n * Username / display name.\n * 3–30 chars, no profanity, strict gibberish detection, no spam.\n * Repeated words allowed (e.g. \"John John\" as a nickname).\n */\nexport function validateUsername(text: string): ValidationResult {\n return createValidator()\n .field('Username')\n .min(3)\n .max(30)\n .noProfanity()\n .noGibberish({ sensitivity: 'strict' })\n .noLowQuality()\n .validate(text);\n}\n\n/**\n * Short text / name fields (product name, company name, form title).\n * 2–100 chars, no profanity, normal gibberish, no spam.\n */\nexport function validateShortText(text: string, fieldName = 'Name'): ValidationResult {\n return createValidator()\n .field(fieldName)\n .min(2)\n .max(100)\n .noProfanity()\n .noGibberish({ sensitivity: 'normal' })\n .noSpam()\n .noLowQuality()\n .validate(text);\n}\n\n/**\n * Bio / description / about me.\n * 10–300 chars, no profanity, no spam, loose gibberish (allows natural language).\n * Repeated words NOT flagged (natural in prose).\n */\nexport function validateBio(text: string): ValidationResult {\n return createValidator()\n .field('Bio')\n .min(10)\n .max(300)\n .noProfanity()\n .noSpam()\n .noGibberish({ sensitivity: 'loose' })\n .validate(text);\n}\n\n/**\n * Long-form text (comment, review, feedback).\n * 5–2000 chars, no profanity, no spam. No gibberish check\n * (long text can contain intentional fragments, code, etc.)\n */\nexport function validateLongText(text: string, fieldName = 'Message'): ValidationResult {\n return createValidator()\n .field(fieldName)\n .min(5)\n .max(2000)\n .noProfanity()\n .noSpam()\n .validate(text);\n}\n\n/**\n * Search query input.\n * 1–200 chars, no spam URLs (but allows short/fragmentary text).\n * Does NOT flag gibberish (code snippets, SKUs, part numbers are valid queries).\n */\nexport function validateSearchQuery(text: string): ValidationResult {\n return createValidator()\n .field('Search query')\n .min(1)\n .max(200)\n .noSpam()\n .validate(text);\n}\n"]}
@@ -149,21 +149,30 @@ function toStructural(t) {
149
149
 
150
150
  // src/core/profanity.ts
151
151
  var PROFANITY_PATTERNS = [
152
- // fuck — also matches 'fack' because leet '4' 'a', so we allow u OR a in slot 2
153
- /\bf+[ua]+c+k+(e[dr]|ing|s|er)?\b/i,
154
- /\bs+h+i+t+(s|te[dr]|ting)?\b/i,
155
- /\bb+i+t+c+h+(e[sd]|ing)?\b/i,
156
- // assuse (^|\s|[^a-z]) instead of \b so it catches "@ss" → "ass" at start of string
157
- /(?:^|(?<=[^a-z]))a+s{2,}(h+o+l+e+s?|e[sd]|ing)?\b/i,
158
- /\bc+u+n+t+s?\b/i,
159
- // dick — exclude as a proper first name before a capitalized surname
160
- /\bd+i+c+k+(s|ed|ing)?\b(?! [A-Z])/i,
161
- /\bp+r+i+c+k+s?\b/i,
162
- /\bb+a+s+t+a+r+d+s?\b/i,
163
- /\bw+h+o+r+e+s?\b/i,
164
- /\bf+a+g+(g+o+t+s?)?\b/i,
165
- /\bn+i+g+(g+e+r+s?|ga+s?)?\b/i,
166
- /\bfrick\b/i
152
+ // fuck — no boundaries needed, no legitimate English words contain 'fuck'
153
+ /f+[ua]+c+k+/i,
154
+ // shit — left boundary to prevent 'mishit'
155
+ /(?<![a-z])s+h+i+t+/i,
156
+ // bitchno boundaries needed
157
+ /b+i+t+c+h+/i,
158
+ // ass — strictly bounded to avoid 'assassin', 'classic', 'bass'
159
+ /(?<![a-z])a+s{2,}(h+o+l+e+s?|e[sd]|ing)?(?![a-z])/i,
160
+ // cunt — left boundary for 'scunthorpe'
161
+ /(?<![a-z])c+u+n+t+/i,
162
+ // dick — strictly bounded to avoid 'dickens', 'medick'
163
+ /(?<![a-z])d+i+c+k+(s|ed|ing)?(?![a-z])(?! [A-Z])/i,
164
+ // prick — strictly bounded to avoid 'prickly'
165
+ /(?<![a-z])p+r+i+c+k+s?(?![a-z])/i,
166
+ // bastard — no boundaries needed
167
+ /b+a+s+t+a+r+d+/i,
168
+ // whore — left boundary
169
+ /(?<![a-z])w+h+o+r+e+/i,
170
+ // fag / faggot — left boundary, right boundary for 'fag' to avoid 'fagus'
171
+ /(?<![a-z])f+a+g+(g+o+t+s?)?(?![a-z])/i,
172
+ // nigger / nigga — left boundary for 'snigger'
173
+ /(?<![a-z])n+i+g+(g+e+r+s?|ga+s?)/i,
174
+ // frick — strictly bounded
175
+ /(?<![a-z])frick(?![a-z])/i
167
176
  ];
168
177
  function containsProfanity(text) {
169
178
  const skeleton = toSkeleton(text);
@@ -276,7 +285,7 @@ function isGibberish(text, sensitivity = "normal") {
276
285
  return words.some((word) => isWordGibberish(word, sensitivity));
277
286
  }
278
287
  function hasRepeatingChars(text) {
279
- return /(.)\1{4,}/.test(toSkeleton(text));
288
+ return /(.)\1{3,}/.test(toSkeleton(text));
280
289
  }
281
290
 
282
291
  // src/core/structure.ts
@@ -502,5 +511,5 @@ function createValidator() {
502
511
  }
503
512
 
504
513
  export { InputShieldValidator, containsProfanity, containsSpam, createValidator, getMatchedProfanityPattern, hasExcessiveSymbols, hasLowAlphabetRatio, hasRepeatedContentWords, hasRepeatingChars, isGibberish, isLowEffortExact, toSkeleton, toStructural };
505
- //# sourceMappingURL=chunk-ADGSP522.mjs.map
506
- //# sourceMappingURL=chunk-ADGSP522.mjs.map
514
+ //# sourceMappingURL=chunk-SX25LIYU.mjs.map
515
+ //# sourceMappingURL=chunk-SX25LIYU.mjs.map