@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 +322 -143
- package/dist/{chunk-WACGX73I.mjs → chunk-ELNY5LCC.mjs} +3 -3
- package/dist/{chunk-WACGX73I.mjs.map → chunk-ELNY5LCC.mjs.map} +1 -1
- package/dist/{chunk-ADGSP522.mjs → chunk-SX25LIYU.mjs} +27 -18
- package/dist/chunk-SX25LIYU.mjs.map +1 -0
- package/dist/{chunk-KVXEPETW.cjs → chunk-VMUP4OS2.cjs} +8 -8
- package/dist/{chunk-KVXEPETW.cjs.map → chunk-VMUP4OS2.cjs.map} +1 -1
- package/dist/{chunk-67CBN3U4.cjs → chunk-XSPW6B5W.cjs} +27 -18
- package/dist/chunk-XSPW6B5W.cjs.map +1 -0
- package/dist/email.cjs +2 -2
- package/dist/email.mjs +1 -1
- package/dist/index.cjs +20 -20
- package/dist/index.mjs +2 -2
- package/dist/zod.cjs +7 -7
- package/dist/zod.mjs +2 -2
- package/package.json +1 -1
- package/dist/chunk-67CBN3U4.cjs.map +0 -1
- package/dist/chunk-ADGSP522.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -5,62 +5,135 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](package.json)
|
|
7
7
|
|
|
8
|
-
>
|
|
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
|
|
13
|
+
## Why input-shield?
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
##
|
|
39
|
+
## Installation
|
|
30
40
|
|
|
31
41
|
```bash
|
|
32
|
-
npm
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
59
|
+
## Quick Start
|
|
40
60
|
|
|
41
|
-
```
|
|
61
|
+
```ts
|
|
42
62
|
import { createValidator } from '@marslanmustafa/input-shield';
|
|
43
63
|
|
|
44
|
-
const
|
|
64
|
+
const validator = createValidator()
|
|
45
65
|
.field('Username')
|
|
46
|
-
.min(3)
|
|
66
|
+
.min(3)
|
|
67
|
+
.max(30)
|
|
47
68
|
.noProfanity()
|
|
48
|
-
.noGibberish(
|
|
49
|
-
.noSpam();
|
|
69
|
+
.noGibberish();
|
|
50
70
|
|
|
51
|
-
const result =
|
|
71
|
+
const result = validator.validate('asdfghjkl');
|
|
52
72
|
|
|
53
73
|
if (!result.isValid) {
|
|
54
|
-
console.log(result.
|
|
55
|
-
console.log(result.
|
|
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
|
-
##
|
|
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
|
-
```
|
|
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('
|
|
73
|
-
|
|
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
|
-
|
|
151
|
+
validateBio('Buy cheap Viagra now! Click here for free money!!!');
|
|
152
|
+
// { isValid: false, reason: 'SPAM', message: '...' }
|
|
81
153
|
|
|
82
|
-
|
|
83
|
-
|
|
154
|
+
validateShortText('asdfasdfasdf');
|
|
155
|
+
// { isValid: false, reason: 'GIBBERISH', message: '...' }
|
|
84
156
|
|
|
85
|
-
|
|
86
|
-
|
|
157
|
+
validateLongText('Hello, this is a proper comment about the topic.');
|
|
158
|
+
// { isValid: true }
|
|
87
159
|
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
174
|
+
## Zod Integration
|
|
175
|
+
|
|
176
|
+
Install Zod separately (`zod >= 3.0.0` is a peer dependency):
|
|
110
177
|
|
|
111
178
|
```bash
|
|
112
|
-
|
|
113
|
-
npm install zod @marslanmustafa/input-shield
|
|
179
|
+
npm install zod
|
|
114
180
|
```
|
|
115
181
|
|
|
116
|
-
|
|
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
|
-
//
|
|
188
|
+
// Custom validator with full fluent chain
|
|
121
189
|
const schema = z.object({
|
|
122
|
-
username:
|
|
123
|
-
bio:
|
|
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
|
-
//
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
//
|
|
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
|
-
##
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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('fuck'); // → "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
|
-
##
|
|
279
|
+
## Core Primitives (Tree-Shakeable)
|
|
169
280
|
|
|
170
|
-
|
|
171
|
-
Returns a new fluent builder instance.
|
|
281
|
+
Use individual functions directly if you need fine-grained control:
|
|
172
282
|
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
| '
|
|
199
|
-
| '
|
|
200
|
-
| '
|
|
201
|
-
| '
|
|
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
|
-
|
|
335
|
+
---
|
|
205
336
|
|
|
206
|
-
|
|
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
|
-
|
|
378
|
+
### Express Middleware
|
|
215
379
|
|
|
216
|
-
|
|
380
|
+
```ts
|
|
381
|
+
import { createValidator } from '@marslanmustafa/input-shield';
|
|
217
382
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
##
|
|
402
|
+
## Contributing
|
|
227
403
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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-
|
|
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-
|
|
22
|
-
//# sourceMappingURL=chunk-
|
|
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-
|
|
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 —
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
/
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
// bitch — no 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{
|
|
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-
|
|
506
|
-
//# sourceMappingURL=chunk-
|
|
514
|
+
//# sourceMappingURL=chunk-SX25LIYU.mjs.map
|
|
515
|
+
//# sourceMappingURL=chunk-SX25LIYU.mjs.map
|