npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@marslanmustafa/input-shield

v0.1.4

Published

One install. No config. Clean inputs. Profanity, spam, gibberish, and homoglyph detection in a single TypeScript-native zero-dependency package.

Readme

@marslanmustafa/input-shield

npm version test coverage license zero dependencies

One install. No config. Clean inputs.
Profanity, spam, gibberish, and homoglyph detection in a single TypeScript-native zero-dependency package.


Why input-shield?

Most profanity filters use a simple word list. They miss:

  • Leet-speak: Fr33 m0ney → normalized → free money → caught
  • Homoglyph attacks: Greek о instead of English o → normalized → caught
  • Unicode evasion: fuck (fullwidth) → normalized → caught
  • Split-tag HTML: <b>f</b>uck → stripped → normalized → caught
  • Gibberish: asdfghjkl passes every regex filter — not this one

input-shield runs a full normalization pipeline before any check, so evasion techniques don't work.

Raw Input
   ↓
Normalization  (leet-speak, homoglyphs, unicode, fullwidth)
   ↓
Skeleton       (removes repeated chars, punctuation tricks)
   ↓
Security Checks (profanity → spam → gibberish → structure)
   ↓
ValidationResult { isValid, reason, message }

Installation

# npm
npm install @marslanmustafa/input-shield

# pnpm
pnpm add @marslanmustafa/input-shield

# yarn
yarn add @marslanmustafa/input-shield

# bun
bun add @marslanmustafa/input-shield

Node.js >= 18 required. Zero runtime dependencies.


Quick Start

import { createValidator } from '@marslanmustafa/input-shield';

const validator = createValidator()
  .field('Username')
  .min(3)
  .max(30)
  .noProfanity()
  .noGibberish();

const result = validator.validate('asdfghjkl');

if (!result.isValid) {
  console.log(result.message); // "Username appears to be gibberish."
  console.log(result.reason);  // "GIBBERISH"
}

Fluent API

Chain as many rules as you need. Every method returns this so chains are fully composable.

import { createValidator } from '@marslanmustafa/input-shield';

// Username
const username = createValidator()
  .field('Username')
  .min(3)
  .max(30)
  .noProfanity()
  .noGibberish({ sensitivity: 'strict' });

// Bio
const bio = createValidator()
  .field('Bio')
  .min(10)
  .max(300)
  .noProfanity()
  .noSpam()
  .noGibberish();

// Search query
const search = createValidator()
  .field('Search')
  .min(1)
  .max(100)
  .noSpam();

// Validate
const result = bio.validate('Buy cheap pills now!!! Click here!!!');
// { isValid: false, reason: 'SPAM', message: 'Bio appears to contain spam.' }

Available chain methods

| Method | Description | |---|---| | .field(name) | Sets the field name used in error messages | | .min(n) | Minimum character length | | .max(n) | Maximum character length | | .noProfanity() | Detects profanity including leet-speak and homoglyphs | | .noSpam() | Detects spam patterns, excessive URLs, repeated phrases | | .noGibberish(options?) | Detects keyboard mash and random character sequences | | .noExcessiveSymbols() | Rejects inputs with too many special characters | | .validate(value) | Runs all checks and returns ValidationResult |


Presets

Ready-to-use validators for common fields. No configuration needed.

import {
  validateUsername,
  validateBio,
  validateShortText,
  validateLongText,
  validateSearchQuery,
} from '@marslanmustafa/input-shield';

validateUsername('h4ck3r_dude');
// { isValid: true }

validateUsername('ааааааааа'); // Cyrillic homoglyphs
// { isValid: false, reason: 'PROFANITY', message: '...' }

validateBio('Buy cheap Viagra now! Click here for free money!!!');
// { isValid: false, reason: 'SPAM', message: '...' }

validateShortText('asdfasdfasdf');
// { isValid: false, reason: 'GIBBERISH', message: '...' }

validateLongText('Hello, this is a proper comment about the topic.');
// { isValid: true }

validateSearchQuery('!!!!!!!!!!!!');
// { isValid: false, reason: 'EXCESSIVE_SYMBOLS', message: '...' }

| Preset | Min | Max | Checks | |---|---|---|---| | validateUsername | 3 | 30 | profanity, gibberish (strict) | | validateBio | 10 | 300 | profanity, spam | | validateShortText | 2 | 100 | profanity, spam, gibberish | | validateLongText | 5 | 2000 | profanity, spam, gibberish | | validateSearchQuery | 1 | 100 | spam, symbols |


Zod Integration

Install Zod separately (zod >= 3.0.0 is a peer dependency):

npm install zod

Import from the /zod subpath to keep Zod out of your main bundle if unused:

import { z } from 'zod';
import { shieldString, zodUsername, zodBio, zodShortText, zodLongText } from '@marslanmustafa/input-shield/zod';

// Custom validator with full fluent chain
const schema = z.object({
  username: shieldString(v => v.field('Username').min(3).max(20).noProfanity().noGibberish()),
  bio:      shieldString(v => v.field('Bio').min(10).max(300).noProfanity().noSpam()),
});

// Or use preset Zod helpers
const schema = z.object({
  username: zodUsername(),
  bio:      zodBio(),
  title:    zodShortText('Title'),
  body:     zodLongText('Body'),
});

// Works with React Hook Form, tRPC, Next.js API routes — anywhere Zod is used
const parsed = schema.safeParse({ username: 'cl3an_user', bio: 'Hello world!' });

Email / Nodemailer Integration

Import from the /email subpath:

import { validateMailContent, stripHtml } from '@marslanmustafa/input-shield/email';

Validate before sending

import nodemailer from 'nodemailer';
import { validateMailContent } from '@marslanmustafa/input-shield/email';

const mail = {
  subject: 'Your order is confirmed',
  html: '<p>Thanks for your purchase! <a href="https://yoursite.com">View order</a></p>',
};

const result = validateMailContent(mail);

if (!result.isValid) {
  // result.field  → 'subject' | 'text' | 'html'
  // result.reason → 'PROFANITY' | 'SPAM' | ...
  // result.message → human-readable string
  throw new Error(`Mail rejected on field "${result.field}": ${result.message}`);
}

await transporter.sendMail({ to: '...', ...mail });

What it catches in HTML emails

import { stripHtml } from '@marslanmustafa/input-shield/email';

// Split-tag evasion
stripHtml('<b>f</b><b>uck</b>');          // → "f uck" → skeleton → "fuck"

// Decimal entity encoding
stripHtml('&#102;&#117;&#99;&#107;');      // → "fuck"

// Spam URLs in href
stripHtml('<a href="https://spam.com">click here</a>'); // → includes URL text

// CSS background trackers
stripHtml('<div style="background:url(https://tracker.spam.com/px)">hi</div>');
// → includes tracker URL for spam check

Custom validator for email

import { createValidator } from '@marslanmustafa/input-shield';
import { validateMailContent } from '@marslanmustafa/input-shield/email';

const strictValidator = createValidator()
  .field('Email content')
  .min(1)
  .max(10000)
  .noProfanity()
  .noSpam();

const result = validateMailContent(
  { subject: 'Hello', html: '<p>Content here</p>' },
  strictValidator
);

Core Primitives (Tree-Shakeable)

Use individual functions directly if you need fine-grained control:

import { toSkeleton, toStructural }              from '@marslanmustafa/input-shield';
import { containsProfanity }                     from '@marslanmustafa/input-shield';
import { containsSpam }                          from '@marslanmustafa/input-shield';
import { isGibberish, hasRepeatingChars }        from '@marslanmustafa/input-shield';
import { hasExcessiveSymbols, hasLowAlphabetRatio } from '@marslanmustafa/input-shield';

// Normalization
toSkeleton('Fr33 m0ney!!!');   // → "free money"
toStructural('fuck');      // → "fuck"

// Individual checks
containsProfanity('h3ll yeah');  // → true
containsSpam('Buy now! Click here! Free!!!'); // → true
isGibberish('asdfghjkl');        // → true
hasRepeatingChars('heeeeello');  // → true
hasExcessiveSymbols('!!!###$$$'); // → true

TypeScript Types

import type {
  ValidationResult,
  FailReason,
  GibberishSensitivity,
  ValidationOptions,
} from '@marslanmustafa/input-shield';

// ValidationResult
type ValidationResult =
  | { isValid: true }
  | { isValid: false; reason: FailReason; message: string };

// FailReason
type FailReason =
  | 'TOO_SHORT'
  | 'TOO_LONG'
  | 'PROFANITY'
  | 'SPAM'
  | 'GIBBERISH'
  | 'EXCESSIVE_SYMBOLS'
  | 'LOW_ALPHABET_RATIO'
  | 'REPEATED_CONTENT'
  | 'LOW_EFFORT';

// GibberishSensitivity
type GibberishSensitivity = 'strict' | 'normal' | 'loose';

Real-World Examples

Next.js API Route

import { validateUsername, validateBio } from '@marslanmustafa/input-shield';

export async function POST(req: Request) {
  const { username, bio } = await req.json();

  const usernameResult = validateUsername(username);
  if (!usernameResult.isValid) {
    return Response.json({ error: usernameResult.message }, { status: 400 });
  }

  const bioResult = validateBio(bio);
  if (!bioResult.isValid) {
    return Response.json({ error: bioResult.message }, { status: 400 });
  }

  // safe to write to DB
}

tRPC Procedure

import { z } from 'zod';
import { zodUsername, zodBio } from '@marslanmustafa/input-shield/zod';

export const updateProfile = publicProcedure
  .input(z.object({
    username: zodUsername(),
    bio: zodBio(),
  }))
  .mutation(async ({ input }) => {
    // input is fully validated and typed
    await db.user.update({ data: input });
  });

Express Middleware

import { createValidator } from '@marslanmustafa/input-shield';

const commentValidator = createValidator()
  .field('Comment')
  .min(5)
  .max(500)
  .noProfanity()
  .noSpam()
  .noGibberish();

app.post('/comments', (req, res) => {
  const result = commentValidator.validate(req.body.comment);
  if (!result.isValid) {
    return res.status(400).json({ error: result.message, reason: result.reason });
  }
  // save comment
});

Contributing

Issues and PRs are welcome. Please open an issue first for major changes.

git clone https://github.com/marslanmustafa/input-shield
cd input-shield
npm install
npm run test:watch

License

MIT © Muhammad Arslan Mustafa