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

@raeven-co/sether

v0.5.7

Published

Streaming PII redaction for AI applications. The hiding place for sensitive data flowing into LLMs. Secrets pack, SSE-aware streaming, audit events, drop-in middlewares for Express / fetch / OpenAI / Anthropic.

Readme

Sether

Hide personal data from your AI before it reaches any LLM provider.

Named for the Hebrew sether (סֵתֶר) — the hiding place. Psalm 32:7.

Sether is a streaming PII-redaction layer that sits between your application and any LLM API. It detects sensitive data (email, phone, SSN, credit-card, IBAN, IP addresses), swaps each match for a stable token before the request leaves your boundary, then restores the original values transparently in the response.

Sether redacts PII locally before it reaches the LLM: the input "Hi, this is Amara Okafor — my account email is amara.okafor@acme.com … card 4242 4242 4242 4242 … call me at +1 (415) 555-2671 … SSN 123-45-6789" becomes "Hi, this is <NAME> — my account email is <EMAIL> … card <CC> … <PHONE> … <SSN>". The token vault stays in your infrastructure and restore() swaps the originals back into the reply.

Your app → Sether redacts locally → LLM. The token vault stays in your infrastructure; restore() swaps the originals back into the reply.

Three lines of TypeScript to wire it up. Works with OpenAI, Anthropic, Cohere, Mistral, Google Gemini, AWS Bedrock, Azure OpenAI, Together, Groq, Ollama, your own fine-tunes — anything that speaks HTTP and streams text. Sether doesn't care who's on the other end; it operates on the text stream.

Status: 0.5.7 — browser-safe @raeven-co/sether/browser entry, opt-in identity pack with multilingual labels (names, DOB, passport, address), secrets pack, SSE/JSON-stream mode, audit events, and drop-in middlewares for Express / fetch / OpenAI / Anthropic. A product of Raeven Company LTD


Why this exists

If your application sends a customer's email, phone number, or any other PII to an LLM provider, that's a sub-processor disclosure under GDPR Article 28. Credit-card data pulls you into PCI DSS scope. Health identifiers trigger HIPAA. The first GDPR enforcement actions tied to AI flows landed in 2025, and the EU AI Act phases in through 2026–2027.

Sether stops the leak at the boundary: sensitive substrings become stable tokens before the bytes leave your process, and restore() swaps them back so your application code does not need to branch on redacted text. Automated per-event regulation tagging and SIEM export are on the Pro / hosted roadmap (see What's coming below) — the npm library focuses on deterministic streaming redaction today.


Try it without installing

The live sandbox runs the same detection engine in your browser — paste any text, watch the PII tokens get swapped in real time:

The sandbox is a browser-only demonstration of @raeven-co/sether. For production, install the package below.


Install

npm install @raeven-co/sether

Requires Node 18+. ESM and CommonJS both supported.

Using Python? The same engine ships as the sether package on PyPI (pip install sether), with sync + async streaming and httpx / ASGI / WSGI / OpenAI / Anthropic integrations. Docs: https://setherai.vercel.app/docs/python.


60-second quickstart

import { Sether } from '@raeven-co/sether';
import { Readable } from 'node:stream';

const sether = new Sether();

// 1. Outgoing request — pipe through sether.redact() before sending to LLM
const userInput = Readable.from(['my email is [email protected]']);
const safeForLLM = userInput.pipe(sether.redact());
// → "my email is <EMAIL_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>"

// 2. LLM response — pipe through sether.restore() before showing the user
const llmResponse = Readable.from([
  'Confirmation sent to <EMAIL_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx>',
]);
const safeForUser = llmResponse.pipe(sether.restore());
// → "Confirmation sent to [email protected]"

The same Sether instance shares its vault between .redact() and .restore(), which is how the round-trip identity is preserved across streaming chunks.

End-to-end round-trip example

import { Sether } from '@raeven-co/sether';
import { Readable } from 'node:stream';

async function streamToString(stream: NodeJS.ReadableStream) {
  let out = '';
  for await (const chunk of stream) out += chunk.toString();
  return out;
}

const sether = new Sether();
const original = 'Contact [email protected] or call +1 415 555 2671.';

const redacted = await streamToString(
  Readable.from([original]).pipe(sether.redact()),
);
// → "Contact <EMAIL_...> or call <PHONE_...>."

const restored = await streamToString(
  Readable.from([redacted]).pipe(sether.restore()),
);
// → "Contact [email protected] or call +1 415 555 2671."

console.log(restored === original); // true

Choose specific detectors

By default Sether runs all built-in detectors. Pass an explicit list to narrow the scope:

import { Sether, emailDetector, ssnDetector } from '@raeven-co/sether';

const sether = new Sether({
  detectors: [emailDetector, ssnDetector], // only these two
});

Built-in detectors (basic pack)

| Detector | Method | Notes | | --- | --- | --- | | emailDetector (EMAIL) | Regex (\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b) | RFC 5321-style local part. ASCII-only — IDN/Unicode addresses are not matched. | | phoneDetector (PHONE) | libphonenumber-js | Matches numbers written with a country code (+1 415 555 2671). For NATIONAL-format numbers ((415) 555-2671, 0803 123 4567) use createPhoneDetector({ defaultCountry: 'US' }) — see below. | | creditCardDetector (CC) | Bounded regex + Luhn check | 13–19 digit numbers passing Luhn. ReDoS-safe. | | ssnDetector (SSN) | Regex + SSA invalid-prefix blacklist | Rejects area 000, 666, and 9XX (ITIN range), group 00, serial 0000. | | ipv4Detector (IPV4) | Strict octet-bounded regex | 0–255 per octet, no leading zeros. | | ipv6Detector (IPV6) | Candidate regex + in-tree isIPv6 validator (equivalent to Node's net.isIPv6, no node:net import) | Known limit: ::1 and IPv4-in-IPv6 (::ffff:192.0.2.1) not matched. | | ibanDetector (IBAN) | Regex + mod-97 checksum | Validates against ISO 13616. |

National-format phone numbers (createPhoneDetector)

Without a country hint, libphonenumber can only recognise numbers that carry an explicit country code — so +1 415 555 2671 is redacted but (415) 555-2671 is not. If your traffic contains national-format numbers for a known region, swap the default PHONE detector for one built with createPhoneDetector (new in 0.5.7):

import { Sether, basicDetectors, createPhoneDetector } from '@raeven-co/sether';

const sether = new Sether({
  detectors: [
    ...basicDetectors.filter((d) => d.type !== 'PHONE'),
    createPhoneDetector({ defaultCountry: 'US' }), // or 'NG', 'GB', …
  ],
});

createPhoneDetector() with no options is exactly the default phoneDetector, so nothing changes unless you opt in.

Identity pack (opt-in — new in 0.3.0)

Names, dates of birth, passport numbers, and addresses have no self-validating shape the way an IBAN or credit card does, so a bare regex for them is a false-positive machine. The identity pack instead uses label-anchored detection: it redacts a value only when it appears with the label that introduces it (Name:, DOB:, Passport No:, Address:), or — for the few distinctive standalone shapes — a structure strong enough to keep false positives low (a street line with a house number + suffix, a UK postcode).

Multilingual (0.4.0): labels are recognised in many languages — Latin-script (Name:/Nom:/Nombre:/Geburtsdatum:/Adresse:…) plus CJK, Cyrillic, and Arabic (名前:, Имя:, الاسم:). Value capture is Unicode-aware throughout, so Nom: José Müller, 名前:田中太郎, and Имя: Иван Петров are all redacted.

It is not part of basicDetectors, so new Sether() behaviour is unchanged. Opt in explicitly:

import { Sether, basicDetectors, identityDetectors } from '@raeven-co/sether';

const sether = new Sether({
  detectors: [...basicDetectors, ...identityDetectors],
});

| Detector | Method | Notes | | --- | --- | --- | | nameDetector (NAME) | Multilingual label/salutation anchor + Unicode-aware capture | Catches labelled names across languages (Name: 田中太郎, Nom: José Müller, Имя: Иван Петров). Does not detect unlabelled names in free prose — that's NER (roadmap). | | dobDetector (DOB) | Label anchor + calendar & plausibility validation | ISO / numeric / written dates; rejects invalid calendar dates and implausible birth years. | | passportDetector (PASSPORT) | Label anchor + format check | 6–9 alphanumerics following a passport label; requires at least one digit. | | addressDetector (ADDRESS) | Label anchor, street-suffix structure, UK postcode | Labelled address lines, <number> <words> <Street/Ave/Rd…>, and UK postcodes. |

Coverage note: the identity pack is strongest on labelled and structured PII (forms, KYC, RAG documents, support tickets). Free-text NER for unlabelled names / organisations / locations is on the roadmap as the separate @raeven-co/sether-ner package.

All detectors implement the Detector interface — you can write your own and pass it via the detectors option.

import type { Detector } from '@raeven-co/sether';

const orderIdDetector: Detector = {
  type: 'ORDER_ID',
  detect(text) {
    const matches = [];
    const re = /\bORD-\d{8}\b/g;
    for (const m of text.matchAll(re)) {
      matches.push({ start: m.index!, end: m.index! + m[0].length, value: m[0] });
    }
    return matches;
  },
};

Token vault (persistence)

Tokens map back to the original values through a vault. Sether ships with an in-memory LRU vault (10 000 entries, 1-hour TTL by default). You can implement the Vault interface yourself to change eviction, encrypt values at rest, or namespace tokens per tenant.

import { Sether, type Vault } from '@raeven-co/sether';

// A custom vault implements the synchronous Vault interface. This one wraps a
// Map and namespaces every token so several tenants can share one store.
class NamespacedVault implements Vault {
  readonly #store = new Map<string, string>();
  readonly #prefix: string;
  constructor(prefix: string) {
    this.#prefix = prefix;
  }
  set(token: string, value: string): void {
    this.#store.set(this.#prefix + token, value);
  }
  get(token: string): string | undefined {
    return this.#store.get(this.#prefix + token);
  }
  has(token: string): boolean {
    return this.#store.has(this.#prefix + token);
  }
  delete(token: string): boolean {
    return this.#store.delete(this.#prefix + token);
  }
  size(): number {
    return this.#store.size;
  }
  clear(): void {
    this.#store.clear();
  }
}

const sether = new Sether({ vault: new NamespacedVault('tenant-42:') });

The Vault interface is synchronous. restore() runs inside a Node streaming Transform that substitutes tokens as the bytes flow through, so it cannot await a lookup per token. A store reached over async I/O (Redis, Postgres) therefore can't be dropped straight into restore() — front it with a synchronous in-process cache that you hydrate before the restore pass, or keep the vault in-process. A first-class async vault adapter is on the roadmap (see What's coming).

The vault stays in your process (or your own backing store if you implement Vault). This package does not phone home — streams are not sent to Raeven. A future optional hosted gateway will be documented separately.


Streaming safety

Sether's redact stream holds back safeDistanceBytes (default 256) at the tail of each chunk, so a PII pattern crossing a chunk boundary is still detected on the next chunk arrival. The restore stream applies the same principle to incoming tokens.

This is verified by property-based tests that partition arbitrary text at random chunk boundaries and assert the redact→restore round-trip is identity (50+ random partitions per CI run).

Values longer than safeDistanceBytes. The held-back tail only guarantees a boundary-crossing match is caught when the whole value fits inside it. A value longer than safeDistanceBytes (256) has not fully arrived when the prefix is emitted, so no detector matches it yet. As of 0.5.5 the redact stream also holds back an in-progress whitespace-free run — every long secret (a JWT, an API key) is whitespace-free — up to a bound of max(safeDistanceBytes × 4, 8192) bytes, so such a value is no longer emitted partially across a chunk boundary. For values larger than that bound, raise safeDistanceBytes (the bound scales with it) or redact complete payloads with redactSync:

const sether = new Sether({ safeDistanceBytes: 4096 }); // e.g. for large JWTs

What's verified in this release

  • Streaming-safe: chunk-boundary round-trip proven by property-based tests
  • ReDoS-safe: all regex literals scanned by safe-regex2 in CI (161 patterns, 0 unsafe)
  • TypeScript strict mode: no any, no implicit types
  • Dual build: ESM + CJS, ≈ 36 KB each
  • CI matrix: Node 18 / 20 / 22 / 24 — lint, typecheck, format, regex-safety, 147 tests, build
  • MIT licensed — fork it, audit it, no vendor lock-in

Honest limitations

Known limitations in this release:

  • Email detection is ASCII-only. IDN/Unicode local parts won't match. Unicode-aware email is on the roadmap.
  • Phone numbers without a country code are not detected by default. +1 415 555 2671 is caught; (415) 555-2671 alone is not — libphonenumber needs a region to parse national formats. Opt in with createPhoneDetector({ defaultCountry: 'US' }) (see Built-in detectors).
  • IPv6 ::1 (loopback) is not detected. Candidate regex requires 4+ chars. Loopback isn't customer PII, but flag it in your audit logs if it matters for your threat model.
  • Credit-card regex is permissive. Anything 13–23 chars of digits/spaces/dashes is a candidate, then validated by Luhn. False positives in dense numeric content are possible.
  • Names / DOB / passport / address are label-anchored, not free-text NER. The opt-in identity pack (0.3.0) catches these when they appear with a label or distinctive structure. Unlabelled names / organisations / locations in running prose need the ONNX NER model shipping separately as @raeven-co/sether-ner.
  • Very large whitespace-free values split across chunk boundaries. The streaming redactor holds an in-progress value back up to max(safeDistanceBytes × 4, 8192) bytes; a single value larger than that bound, split across two chunks, can still be emitted partially. Raise safeDistanceBytes or use redactSync on complete payloads. See Streaming safety.
  • No production benchmarks yet. Throughput numbers (vs Microsoft Presidio) will land alongside the NER package.

What's new in 0.3.0

The headline of this release is the opt-in identity detector pack — label-anchored detection for names, dates of birth, passport numbers, and addresses (documented under Identity pack above). It is not part of basicDetectors, so new Sether() behaves exactly as it did in 0.2.x — existing installs are unaffected unless they opt in.

This release also makes wrapFetch's fetchImpl required — it no longer falls back to globalThis.fetch. That's a small breaking change for anyone who relied on the implicit fallback; the one-line migration is to pass fetchImpl: fetch. Full notes in the CHANGELOG.


Capabilities: secrets, SSE, middlewares & audit

These shipped in 0.2.0 and are part of the stable API.

Secrets detector pack (secretsDetectors)

Eight new detectors for the credential classes engineers most often leak into prompts:

import { Sether, basicDetectors, secretsDetectors } from '@raeven-co/sether';

const sether = new Sether({
  detectors: [...basicDetectors, ...secretsDetectors],
});

Detectors: awsAccessKeyDetector, openaiKeyDetector, anthropicKeyDetector, githubPatDetector (classic + fine-grained), slackTokenDetector, stripeKeyDetector (live/test/webhook), jwtDetector, highEntropyDetector (Shannon entropy ≥ 3.5 bits/char).

SSE / JSON-stream mode

OpenAI / Anthropic streaming responses come back as Server-Sent Events. The SSE-aware stream redacts payloads inside data: lines while preserving the data: / event: / id: / retry: framing and blank-line separators verbatim.

import { createSSERedactStream, basicDetectors, MemoryVault } from '@raeven-co/sether';

const vault = new MemoryVault();
openaiResponse.body.pipe(createSSERedactStream({ detectors: basicDetectors, vault }));

Drop-in middlewares

Four ways to wire Sether into an existing app without rewriting handlers:

// Generic fetch — pass your runtime's fetch explicitly. Sether does not
// reach for globalThis.fetch implicitly (keeps the supply-chain surface
// honest: the caller declares the network capability, not the library).
import { wrapFetch } from '@raeven-co/sether';
const safeFetch = wrapFetch({
  detectors: sether.detectors,
  vault: sether.vault,
  fetchImpl: fetch,        //  Node 18+, or your polyfill / undici / mock
});

// Express
import express from 'express';
import { createExpressMiddleware } from '@raeven-co/sether';
const app = express();
app.use(express.json());
app.use(createExpressMiddleware({ detectors: sether.detectors, vault: sether.vault }));

// OpenAI SDK — bring your own client (Sether never imports `openai`)
import OpenAI from 'openai';
import { wrapOpenAI } from '@raeven-co/sether';
const openai = wrapOpenAI(new OpenAI({ apiKey }), {
  detectors: sether.detectors,
  vault: sether.vault,
});

// Anthropic SDK — bring your own client (Sether never imports `@anthropic-ai/sdk`)
import Anthropic from '@anthropic-ai/sdk';
import { wrapAnthropic } from '@raeven-co/sether';
const anthropic = wrapAnthropic(new Anthropic({ apiKey }), {
  detectors: sether.detectors,
  vault: sether.vault,
});

The wrappers are structurally typed — Sether never imports openai or @anthropic-ai/sdk (not at runtime, not as a type). You pass your own client instance; any object matching the chat.completions.create / messages.create shape works. They are not declared as dependencies or peer dependencies, so Sether's install footprint stays a single package (libphonenumber-js) and its supply-chain surface stays minimal.

Audit-event schema

Every redaction can emit a structured AuditEvent that maps to the regulation it satisfies (GDPR Art. 28, SOC 2 CC6.7, HIPAA §164.312, PCI DSS, etc. — see DEFAULT_REGULATION_MAPPINGS). Ship-ready writers:

import { ConsoleAuditSink, MemoryAuditSink, DEFAULT_REGULATION_MAPPINGS } from '@raeven-co/sether';

ConsoleAuditSink writes JSONL to stderr. MemoryAuditSink accumulates events for tests and the browser sandbox. The original value is never carried in an event — only its length. Persistence (Postgres / D1 / SIEM export) lives in the hosted Pro tier; the schema is the same on both sides so promoting from local-only to hosted doesn't reshape events.

redactSync(text, { detectors, vault }) / restoreSync(text, { vault })

Synchronous one-shot redaction and restoration for cases where you have the full text in hand (a JSON field, a log line, an SSE payload) and don't need chunk-boundary buffering. restoreSync is the mirror of redactSync: it swaps <TYPE_…> tokens back to their originals via the vault, leaving any token with no vault entry untouched.

import { redactSync, restoreSync, basicDetectors, MemoryVault } from '@raeven-co/sether';

const vault = new MemoryVault();
const safe = redactSync('email [email protected]', { detectors: basicDetectors, vault });
// → 'email <EMAIL_…>'
const back = restoreSync(safe, { vault });
// → 'email [email protected]'

Use createRedactStream / createRestoreStream for input that may span chunk boundaries.


What's coming (next / Pro hosted tier)

  • Free-text NER detectors — unlabelled names, organisations, and locations in running prose. Ships as a separate @raeven-co/sether-ner package to keep the core install lean (avoids the ~30 MB native ONNX runtime). The 0.3.0 identity pack already covers these entities when they are labelled; NER extends them to free text.
  • Vault adapters as reference examples — Redis and Postgres patterns documented in the repo, not bundled (the Vault interface already supports BYO).
  • Compliance reports mapped to SOC 2 / GDPR / HIPAA controls — Pro hosted tier.
  • Audit log persistence + SIEM export — Pro hosted tier.
  • Benchmarks vs Microsoft Presidio — alongside the sether-ner release.

Track progress: https://github.com/raeven-co/sether


Roadmap to 1.0.0

  • [x] Chunk-boundary-safe streaming Transform
  • [x] Property-based redact↔restore identity tests
  • [x] safe-regex2 enforcement in CI
  • [x] Dual ESM + CJS build
  • [x] Detector pack: email, phone, CC, SSN, IPv4, IPv6, IBAN
  • [x] Detector pack: secrets (AWS, OpenAI, Anthropic, GitHub, Slack, Stripe, JWT)
  • [x] JSON-stream mode for SSE / JSON LLM streams
  • [x] Drop-in middlewares (Express, fetch, OpenAI SDK, Anthropic SDK)
  • [x] Detector pack: identity (label-anchored name / DOB / passport / address)
  • [ ] Detector pack: ONNX-based NER for free-text names / orgs / locations (@raeven-co/sether-ner)
  • [ ] Pluggable vault adapters (Redis, Postgres)
  • [ ] Benchmarks vs Microsoft Presidio (committed in repo)
  • [ ] Migration guide from redact-ai-stream 1.x

License

MIT © Godfrey Lebo / Raeven Company LTD

Reporting security issues

Email [email protected]. Do not file public GitHub issues for security findings. See SECURITY.md for the full policy.

Links