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

sensitive-words-dfa-filter

v2.1.0

Published

A trie-based sensitive-word filter for Node.js. It scans text against a keyword dictionary and masks matches, while also detecting common obfuscation tricks such as inserting spaces, symbols, digits, or letters between the characters of a keyword.

Readme

sensitive-words-filter

NPM Version NPM Downloads CI

A trie-based sensitive-word filter for Node.js. It scans text against a keyword dictionary and masks matches, while also detecting common obfuscation tricks such as inserting spaces, symbols, digits, or letters between the characters of a keyword.

Features

  • Trie-based engine — keywords are compiled into a prefix tree; scanning starts from every character with per-match filler skipping
  • Obfuscation detection — matches keywords even when filler characters are inserted between keyword characters
  • Longest-match — when keywords overlap (e.g. AB and ABC), the longer match wins
  • Case-insensitive — keywords and input text are matched case-insensitively
  • Flexible keyword loading — pass an array, a single file, or a directory of keyword files
  • Singleton or instancesWordFilter.instance() for a shared instance, or new WordFilter() for independent dictionaries
  • Word-boundary mode for pure-English text — optional word-segmentation mode for English-only environments; restricts ASCII keywords to word-boundary matches (opt-in, off by default)
  • Per-keyword lenient mode — tag a keyword as { word: 'drug', lenient: true } to let that word tolerate letter/digit fillers (d1rug matches drug); every other keyword stays strict

Install

npm install sensitive-words-dfa-filter --save

TypeScript users get full type hints out of the box — the package ships with type declarations.

Quick start

const WordFilter = require('sensitive-words-dfa-filter');

const filter = WordFilter.instance();

// Load keywords from an array (or a file / directory, see below)
filter.init(['AB', 'ABC', '治国']);

// Basic matching
console.log(filter.replace('This is "AB|A B|AAB|A1B|A@B" filter word!'));
// This is "**|**|A**|A1B|**" filter word!

console.log(filter.replace('This is "ABC|A B C|A1B1C|A@B@C" filter word!'));
// This is "***|***|A1B1C|***" filter word!

// Custom replacement character
console.log(filter.replace('This is "治国|治 国|治A国|治1国|治@国" filter word!', '?'));
// This is "??|??|??|??|??" filter word!

// Matching modes
// Word-boundary mode: for pure-English environments — ASCII keywords match
// only at word boundaries (admin no longer matches administrator).
filter.init(['admin'], { wordBoundary: true });
console.log(filter.replace('administrator admin admin123'));
// administrator ***** admin123

// Both knobs compose in one call: wordBoundary is global, lenient is per
// keyword — d1rug matches drug (up to 3 letter/digit fillers), while
// drugabc does not match drug anymore because of the word boundary.
filter.init(['admin', { word: 'drug', lenient: true }], { wordBoundary: true });
console.log(filter.replace('d1rug admin drugabc'));
// **** ***** drugabc

Loading keywords

init() accepts either an array of keywords or a path:

| Input | Example | Notes | |---|---|---| | Array | filter.init(['赌博', '诈骗']) | Simple in-code keyword list | | File | filter.init('/path/to/keywords.txt') | Reads the file; keywords separated by or newlines | | Directory | filter.init('/path/to/keywords') | Reads every file inside the directory, including subdirectories |

Example keyword file:

赌博、诈骗、禁言、敏感词
spam
cheat

Empty entries and surrounding whitespace are ignored.

API

WordFilter.instance()

Returns the shared singleton instance.

new WordFilter()

Creates an independent filter with its own dictionary — useful when different parts of an application need different keyword sets.

init(keywords: (string | { word: string; lenient?: boolean })[], options?: { wordBoundary?: boolean }): void

Synchronously builds the matching dictionary.

  • keywords — an array of keyword strings or entries ({ word, lenient }), or a path to a keyword file/directory.
  • lenient (per entry) — when true, this keyword tolerates up to 3 letters/digits as fillers even in pure-ASCII matches (d1rug matches drug, d1r2u3g too; d11r22u33g does not). The cap prevents the keyword's first letter from matching across unrelated words (e.g. admin drug does not match drug from the d of admin). Off by default; note this also means A1B matches AB if AB is tagged lenient.
  • options.wordBoundary — a word-segmentation mode intended for pure-English environments. When true, pure-ASCII keywords only match at word boundaries: the character immediately before and after the keyword must not be a letter, digit, or underscore (same definition as \b), so admin no longer matches inside administrator. Off by default; CJK keywords are not affected. Composes with per-keyword lenient entries in the same call.
  • Calling init() again replaces the existing dictionary.

replace(searchValue: string, replaceValue?: string): string

Scans searchValue and replaces every matched keyword with replaceValue repeated once per matched keyword character (default *).

  • Returns searchValue unchanged if the filter has not been initialized.

getCacheStats(): { size: number; entries: string[] }

Returns the size of the preloaded "skippable-character" cache and the cached characters (useful for debugging and performance monitoring).

clearCache(): void

Fully resets the filter: clears the preloaded character cache, the keyword dictionary, and the initialization state. Call init() again before using the filter.

Matching rules

  • Case-insensitive: Text matches keyword text.
  • Longest match wins: with keywords AB and ABC, input ABC is masked as ***.
  • Filler characters can be skipped between keyword characters:

| Filler type | Skipped by default? | Example | |---|---|---| | Symbols (@, #, ., ~, ...) | Always | A@B matches AB | | Spaces / whitespace | Always | A B matches AB | | CJK radicals | Always | 中灬国 matches 中国 | | Full-width spaces / punctuation ( , , , ...) | Always | 治 国 matches 治国 | | Full-width letters / digits (A-Z, a-z, 0-9) | Always, as fillers (not normalized) | 治A国 matches 治国; AB does not match AB | | Digits (0-9) | Only when the matched part already contains non-ASCII characters | 治1国 matches 治国; A1B does not match AB | | Letters (a-z, A-Z) | Only when the matched part already contains non-ASCII characters | 治A国 matches 治国; AAB becomes A** |

The non-ASCII rule prevents false positives in pure English text: with keyword text, the input This is text only masks text itself — the leading words are left untouched.

  • English keywords match as plain substrings by defaultadmin matches inside administrator, and sex inside sexy. For pure-English environments, pass { wordBoundary: true } to init() to switch to word-segmentation mode: ASCII keywords only match at word boundaries (e.g. init(['admin'], { wordBoundary: true }) masks the admin but leaves administrator and admin123 untouched). CJK keywords are not affected; ASCII keywords are boundary-checked regardless of the surrounding text language.
  • Per-keyword lenient mode — tag a keyword as { word: 'drug', lenient: true } to allow up to 3 letter/digit fillers for that keyword even when the match is pure ASCII (d1rug matches drug). The cap stops cross-word matches (admin drug does not match drug via the d of admin). This is opt-in because it is a precision tradeoff: if AB is tagged lenient, A1B matches AB too.
  • Single-character ASCII keywords are ignored (e.g. a) to avoid over-matching; single-character CJK keywords (e.g. ) are supported.
  • Overlapping matches are merged into a single masked region: with keywords ABC and BCD, the input A B C D is masked as ****.

Performance

Measured locally (Node.js 24, v2.1.0) with an 11-keyword dictionary and ~1MB inputs:

| Input | Time | Throughput | |---|---|---| | Chinese text | ~540 ms | ~1.9 MB/s | | English text | ~190 ms | ~5.2 MB/s | | Mixed Chinese/English | ~370 ms | ~2.7 MB/s |

Numbers vary by machine, dictionary size, and hit rate — run npm run bench on your own hardware. The engine is a trie with per-position scanning (not an Aho-Corasick automaton), which is plenty for interactive filtering but not for multi-GB/s pipelines.

A pathological case — a single 500-character keyword over 20KB of repetitive text — takes ~0.4s, since nearly every position walks the whole keyword path.

Limitations

  • Substring matching by default — English keywords match inside longer words (admin matches administrator). In pure-English environments, enable { wordBoundary: true } (see Matching rules) to switch to word-segmentation mode.
  • No full-width normalization — full-width AB does not match keyword AB; full-width letters/digits are only treated as skippable fillers between keyword characters.
  • Curated obfuscation coverage — the skippable-filler set is a fixed list (symbols, whitespace, CJK radicals, full-width characters). Exotic tricks such as emoji, zero-width characters, or homoglyphs are not covered.
  • Memory — each replace() call allocates a small amount of memory proportional to the input length (coverage/star arrays); fine for typical messages, worth noting for very large documents.
  • Pathological inputs — a very long keyword over highly repetitive text can approach O(n × keyword length).

Development

npm install
npm test        # compile and run all test suites
npm run build   # compile src/index.ts to build/index.js
npm run bench   # local performance benchmark

Test suites:

  • tests/WordsFilter.test.ts — 100 fixture-based integration cases (fails the run on any failure)
  • tests/WordsFilter-New.test.ts — 60 additional fixture-based cases (fails the run on any failure)
  • tests/unit.test.ts — 33 focused unit tests with assertions

License

MIT