
Security News
/Research
Fake Corepack Site Distributes Infostealer and Proxyware to Developers
A fake corepack.org site is impersonating the Node.js tool and delivers an infostealer and proxyware to developers who download it.
@stll/aho-corasick
Advanced tools
NAPI-RS bindings to the Rust aho-corasick crate for Node.js and Bun.
Multi-pattern string searching in linear time. Built on BurntSushi's aho-corasick (the same engine that powers ripgrep), exposed to JavaScript via NAPI-RS.
npm install @stll/aho-corasick
# or
bun add @stll/aho-corasick
Prebuilt binaries are available for:
| Platform | Architecture |
|---|---|
| macOS | x64, arm64 |
| Linux (glibc) | x64, arm64 |
| Linux (musl) | x64 |
| Windows | x64 |
import { AhoCorasick } from "@stll/aho-corasick";
const ac = new AhoCorasick(["foo", "bar", "baz"]);
// Check for any match
ac.isMatch("hello foo world"); // true
// Find all non-overlapping matches
ac.findIter("foo bar baz");
// [
// { pattern: 0, start: 0, end: 3, text: "foo" },
// { pattern: 1, start: 4, end: 7, text: "bar" },
// { pattern: 2, start: 8, end: 11, text: "baz" },
// ]
// Find overlapping matches
ac.findOverlappingIter("foobar");
// Replace matches
ac.replaceAll("foo bar", ["FOO", "BAR", "BAZ"]);
// "FOO BAR"
const ac = new AhoCorasick(patterns, {
// Match semantics (default: "leftmost-first")
matchKind: "leftmost-longest",
// ASCII case-insensitive (default: false)
caseInsensitive: true,
// Only match whole words (default: false)
// Unicode-aware; CJK always passes
wholeWords: true,
});
For processing large files or streams chunk by chunk:
import { StreamMatcher } from "@stll/aho-corasick";
const sm = new StreamMatcher(["needle", "haystack"]);
for await (const chunk of readableStream) {
const matches = sm.write(chunk);
for (const m of matches) {
console.log(
`Pattern ${m.pattern} ` + `at ${m.start}..${m.end}`,
);
}
}
sm.flush(); // finalize stream state
sm.reset(); // reuse for another stream
StreamMatcher automatically handles overlap
between chunks so that matches spanning chunk
boundaries are found.
To organize patterns into named groups (e.g., for
entity recognition), use the pattern index as a
lookup key into a parallel array:
const GROUPS = {
LEGAL_FORM: ["s.r.o.", "GmbH", "LLC"],
CURRENCY: ["EUR", "USD", "CZK"],
};
const patterns: string[] = [];
const tag: string[] = [];
for (const [group, terms] of Object.entries(GROUPS)) {
for (const term of terms) {
patterns.push(term);
tag.push(group);
}
}
const ac = new AhoCorasick(patterns, {
wholeWords: true,
});
for (const m of ac.findIter(text)) {
console.log(m.text, tag[m.pattern]);
// "GmbH" "LEGAL_FORM"
// "EUR" "CURRENCY"
}
tag[m.pattern] is a single array index lookup
(O(1), no hashing).
Measured on Apple M3, 24 GB RAM, macOS 25.3.0, Bun 1.3.10. Automaton pre-built; times are search-only averaged over multiple runs.
Corpora: Canterbury Large Corpus (ASCII), Leipzig Corpora Collection (Unicode).
Run locally:
bun run bench:install && bun run bench:download && bun run bench:all
| Haystack | Patterns | @stll | modern-ahocorasick | ahocorasick | @monyone | @tanishiking |
|---|---|---|---|---|---|---|
| bible.txt (4.0 MB) | 20 legal terms | 5 ms | 444 ms | 130 ms | 129 ms | 585 ms |
| E.coli (4.6 MB) | 16 DNA codons | 2 ms | 288 ms | 16 ms | 135 ms | 637 ms |
| world192.txt (2.5 MB) | 20 legal terms | 1 ms | 300 ms | 121 ms | 71 ms | 312 ms |
| bible.txt (4.0 MB) | 1 pattern | 1 ms | 254 ms | 19 ms | 53 ms | 420 ms |
| Haystack | Patterns | @stll | modern-ahocorasick | ahocorasick | @monyone | @tanishiking |
|---|---|---|---|---|---|---|
| Czech news 2024 (4.8 MB) | 10 legal terms | 23 ms | 563 ms | 271 ms | 94 ms | 652 ms |
| Turkish news 2024 (5.4 MB) | 10 terms | 28 ms | 724 ms | 358 ms | 158 ms | 731 ms |
| Japanese newscrawl 2019 (2.4 MB) | 10 terms | 16 ms | 521 ms | 411 ms | 168 ms | 620 ms |
| Chinese Wikipedia 2021 (2.0 MB) | 10 terms | 15 ms | 361 ms | 323 ms | 94 ms | 607 ms |
| German news 2024 (5.5 MB) | 10 terms | 13 ms | 742 ms | 229 ms | 107 ms | 846 ms |
The same Rust code compiles to WASM via
wasm32-wasip1-threads. Bundlers (Vite, Webpack)
auto-select the WASM build for browser targets.
| Haystack | @stll WASM | @stll native | Best pure JS |
|---|---|---|---|
| bible.txt (4.0 MB) | 34 ms | 4 ms | 186 ms |
| Czech news (4.8 MB) | 61 ms | 17 ms | 208 ms |
WASM is 4-8x slower than native, but 3-6x faster than the best pure-JS alternative; in browsers where native modules are unavailable, it is the fastest option.
All match counts verified equal across libraries.
Match offsets are UTF-16 code unit indices,
compatible with String.prototype.slice().
AhoCorasick| Method | Returns | Description |
|---|---|---|
new AhoCorasick(patterns, options?) | instance | Build automaton |
.findIter(haystack) | Match[] | Non-overlapping matches |
.findOverlappingIter(haystack) | Match[] | All overlapping matches |
.isMatch(haystack) | boolean | Any pattern matches? |
.replaceAll(haystack, replacements) | string | Replace matched patterns |
.findIterBuf(haystack) | ByteMatch[] | Matches in a Buffer / Uint8Array (byte offsets) |
.isMatchBuf(haystack) | boolean | Any pattern matches in a Buffer / Uint8Array? |
.patternCount | number | Number of patterns |
StreamMatcher| Method | Returns | Description |
|---|---|---|
new StreamMatcher(patterns, options?) | instance | Build streaming matcher |
.write(chunk) | ByteMatch[] | Feed chunk, get global byte-offset matches |
.flush() | ByteMatch[] | Finalize stream |
.reset() | void | Reset for reuse |
type MatchKind = "leftmost-first" | "leftmost-longest";
type Options = {
matchKind?: MatchKind;
caseInsensitive?: boolean;
wholeWords?: boolean;
dfa?: boolean;
};
// Returned by string methods (findIter, etc.)
type Match = {
pattern: number; // index into patterns array
start: number; // UTF-16 code unit offset
end: number; // exclusive
text: string; // matched substring
};
// Returned by Buffer/streaming methods
type ByteMatch = {
pattern: number; // index into patterns array
start: number; // byte offset
end: number; // exclusive
};
new AhoCorasick([...]) throws if the
automaton cannot be built (e.g. patterns exceed
internal size limits).replaceAll(haystack, replacements) throws if
replacements.length !== patternCount.A-Z to a-z but
does not handle Unicode case folding (Turkish
İ/ı, German ß/ss, etc.). This is a
documented upstream limitation.SharedArrayBuffer. Browser
builds need Cross-Origin-Opener-Policy: same-origin
and Cross-Origin-Embedder-Policy: require-corp
headers. Edge runtimes without WASM support
(some Cloudflare Workers configurations) are
not supported.This package is a thin binding layer. The hard work is done by:
# Install dependencies
bun install
# Build native module (requires Rust toolchain)
bun run build
# Run tests (113 tests, including Unicode edge cases)
bun test
# Download benchmark corpora
bun run bench:download
# Install benchmark dependencies (alternatives)
bun run bench:install
# Run benchmarks
bun run bench:speed # Canterbury corpus
bun run bench:unicode # Leipzig corpora
bun run bench:correctness # cross-library comparison
bun run bench:all # all three
# Lint & format
bun run lint
bun run format
FAQs
Exact many-pattern string search for Node.js and Bun via Rust's aho-corasick engine exposed through NAPI-RS.
We found that @stll/aho-corasick demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
/Research
A fake corepack.org site is impersonating the Node.js tool and delivers an infostealer and proxyware to developers who download it.

Research
/Security News
A large-scale campaign abused GitHub Actions in compromised repositories to exploit CVE-2026-41940 in cPanel and WHM and steal server credentials.

Security News
Five frontier LLMs generated the same nonexistent package names, leaving 53 available for potential slopsquatting across PyPI and npm.