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

@gnldev/processors

v0.5.0

Published

Input/output processors for @gnldev/durable agents: PII redaction, moderation, tool-filter. Durability-infused (journaled).

Readme

@gnldev/processors

Input/output processors for @gnldev/durable agents: PII masking, moderation, tool filtering, safety. Built-ins inherit durability — input processors run before persistInput (masking is journaled → doesn't run again on resume).

Install: pnpm add @gnldev/processors — or use it from a repo clone: pnpm install && pnpm -r build.

npm i @gnldev/processors   # peer: @gnldev/durable
import { runDurable } from '@gnldev/durable';
import { piiRedactor, moderationProcessor, toolFilter } from '@gnldev/processors';

await runDurable({
  runId: 'r1', journal, model, prompt: '...',
  processors: [
    piiRedactor(),                                    // mask email/phone/card
    moderationProcessor({ blocklist: ['...'] }),      // tripwire on blocked content
    toolFilter({ allow: ['searchPolicy', 'lookupOrder'] }), // restrict the tool set the model sees
  ],
});

API

  • piiRedactor(opts?) · moderationProcessor(opts?) · toolFilter(opts?)
  • tokenLimit(...) · promptInjectionDetector(...) · outputLimit(...)
  • piiTextRedactor(opts?) → (text: string) => string — the same masking as piiRedactor, as a plain string function, for text that never passes through a processor at all. A Processor only sees processInput/processOutput/processToolResult; a run's failure message is written by recordRunOutcome in @gnldev/durable and no processor is consulted about it, yet it can carry user data (providers echo the offending input back when refusing). Wire it wherever that message is shipped onward — @gnldev/otel's redact option is the case it was added for. Takes types/mask and shares piiRedactor's defaults, on purpose: two copies of the defaults is how the redacted path and the un-redacted one drift apart.
  • Lower-level: redactString, redactMessages, PII_PATTERNS, PII_VALIDATORS, PiiPattern, luhn, ibanMod97

Checksums: why any sixteen digits is not a card

Some identifiers carry their own check digit, so whether a match is real is arithmetic rather than a guess. creditCard is validated with Luhn and iban with mod-97, and a match that fails its own check is left alone:

| Text | Masked | |---|---| | kart 4111 1111 1111 1111 | yes — Luhn holds | | siparis 1234567812345678 | no — same shape, not a card | | TR330006100519786457841326 | yes — mod-97 holds | | one digit changed | no |

The trade is real: a card typed with a wrong digit fails Luhn and is then not masked. Pass validate: false to mask on shape alone — blunter, corrupts more text, but cannot be talked out of masking anything.

phone has no checksum to appeal to, so it is bounded by shape instead: 7–15 digits, and short groups once a number is split. That replaced a pattern which counted characters, and so masked 2024-01-15 10 out of a timestamp, the whole of 1.2.3 - 4.5.6, and a run id — text corruption in exactly the payload it most often runs over. It is checked against 24 written forms across a dozen countries, and against non-PII text that has to survive — both directions, because a first attempt verified against a set chosen after the rule was written passed while +49 30 12345678, +90 5321112233 and 0212 5551234 were going through unmasked.

A match that fails its checksum is skipped, not consumed. That matters: with a plain String.replace, a candidate spanning 1234567890 555-123-4567 failed the digit count and took the real number out of reach of every later pattern — a leak caused by the validator meant to prevent one.

Masking an identifier the built-ins cannot name

National ids are a per-country long tail, so they are not shipped here — extraPatterns is how you add your own, with the same checksum power the built-ins have:

import { piiRedactor } from '@gnldev/processors';

piiRedactor({
  extraPatterns: [
    { name: 'mrn', pattern: /\bMRN-\d{6}\b/g },
    // `test` is where the identifier's own checksum goes, so a custom pattern is as precise as a built-in.
    { name: 'tckn', pattern: /\b\d{11}\b/g, mask: '[TCKN]', test: (v) => v[10] === String(
      ([...v.slice(0, 10)].reduce((n, d) => n + Number(d), 0)) % 10) },
  ],
});

They run before the built-ins, and that ordering is load-bearing: phone would otherwise reach an 11-digit national id first and mask it under the wrong name — which would also make the audit report say the wrong type. Custom names appear in recordProcessorReport alongside the built-in ones, and the counts come off the same call that performs the redaction, so the report cannot describe a masking that did not run. A pattern without the g flag gets it added, rather than quietly masking only the first occurrence.

A name in types with no pattern behind it is refused at construction instead of masking nothing in silence; piiTextRedactor takes the same options.

  • Re-exported: the Processor type + ProcessorTripwire (importable from a single package)

How it works

The processor pipeline is passed to runDurable/createGnl config. Pure transforms (masking) are deterministic; non-deterministic ones are journaled via ctx.step → same result on replay. When a tripwire fires, the run stops safely.

Limitations — an honest warning

moderationProcessor, promptInjectionDetector, piiRedactor, and untrustedToolContent use naive substring/regex matching. This means they:

  • Are easily bypassed — by adding whitespace/punctuation, typos, unicode homoglyphs, paraphrasing, encoding (base64, etc.), or another language.
  • Are NOT a real security/compliance boundary — prompt-injection defense is still an unsolved problem in LLM safety; PII regexes don't guarantee GDPR/HIPAA/PCI-DSS-level detection.
  • Are designed as a first line of defense / noise-reduction layer. For a critical flow (payment, data deletion, external authorization), don't use these as the ONLY protection mechanism — combine them with additional layers like a model-based judge, human approval, or least-privilege design.

For details, see the JSDoc on the relevant functions (src/moderation.ts, src/safety.ts, src/pii.ts, src/redact.ts).

License

Apache-2.0 — see LICENSE.