Developer documentation / TypeScript

@raeven-co/sether

Streaming PII redaction for AI applications. Tokenise sensitive data before it reaches any LLM, restore it in the reply. MIT licensed, one dependency, Node 18+.

  • v0.8.0
  • 252 tests
  • ReDoS-scanned · 0 unsafe
  • ESM + CJS
  • 1 dependency

Full MIT library. Decoy and restore APIs included.

Overview

Sether sits between your application and any LLM API: OpenAI, Anthropic, Gemini, Bedrock, your own fine-tunes. It detects sensitive data, swaps each match for a random token like <EMAIL_…> before the request leaves your boundary, then restore() swaps the originals back into the response. Your application code never has to branch on redacted text.

The SDK round trip

Same task. Fewer real details.

01 Your application
ORIGINAL TEXT

Write to alice@example.com.

Start with useful context and a supported email match.

02 Replace the match
TEXT FOR YOUR MODEL

Write to <EMAIL_…>.

03 Restore a known value
EXAMPLE REPLY · RESTORED

To: alice@example.com.

Recognized replacement + matching retained vault.

Your app owns the mapping.Fictional text and illustrative replacements. Your app controls sending. Keep the same vault for restore(). Vault details ↗

Detection is deterministic (validated patterns — Luhn, mod-97, SSA blacklists — not ML guesses), streaming-safe (chunk-boundary correct, property-tested), and local (no network calls; the package has a single dependency, libphonenumber-js).

Prefer Python? The same engine ships as the sether package on PyPI — sync and async streaming, with httpx / ASGI / WSGI / OpenAI / Anthropic integrations and the same token format. Since 0.4.0 it also has the alias engine, multi-region phone detection and JSON-safe restore. See the Python docs.

Install

npm install @raeven-co/sether

Node 18+. ESM and CommonJS both supported. The package makes zero network calls and ships pure-ASCII bundles (supply-chain scanner friendly).

Quickstart

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

const sether = new Sether();

// 1. Outgoing request — redact before it reaches the LLM
const safeForLLM = Readable
  .from(['my email is alice@example.com'])
  .pipe(sether.redact());
// → "my email is <EMAIL_…>"

// 2. LLM response — restore before it reaches your user
const safeForUser = llmResponseStream.pipe(sether.restore());
// → "Confirmation sent to alice@example.com"

The same Sether instance shares its vault between redact() and restore() — that is how the round-trip identity is preserved across streaming chunks. One instance per request/conversation is the standard pattern.

For non-streaming text there is a synchronous one-shot: redactSync(text, { detectors, vault }).

Detectors (basic pack)

new Sether() runs the basic pack by default. 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
});
ExportToken typeMethod
emailDetectorEMAILRFC 5321-style regex. ASCII local parts.
phoneDetectorPHONElibphonenumber-js — international parsing.
creditCardDetectorCCBounded regex + Luhn check. ReDoS-safe.
ssnDetectorSSNRegex + SSA invalid-prefix blacklist.
ipv4DetectorIPV4Strict octet-bounded (0–255, no leading zeros).
ipv6DetectorIPV6Candidate regex + in-tree isIPv6 validator.
ibanDetectorIBANRegex + ISO 13616 mod-97 checksum.

0.7.0 adds createMultiRegionPhoneDetector, which catches national-format numbers like 0801 234 5678 or (415) 555-2671 across multiple regions at once, not just international + formats.

Custom detectors are plain objects: { type: string; detect(text): { start, end, value }[] } — pure, synchronous, deterministic.

Identity pack (opt-in)

Names, dates of birth, passport numbers, and addresses have no self-validating shape, so a bare regex would be a false-positive machine. The identity pack uses label-anchored detection — it redacts a value only when it appears with the label that introduces it (Name:, DOB:, Passport No:, Address:), in many languages: Latin-script labels plus CJK, Cyrillic, and Arabic (名前:, Имя:, الاسم:).

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

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

Exports: nameDetector, dobDetector, passportDetector, addressDetector. Unlabelled names in free prose are the job of free-text NER.

0.7.0 broadens the pack: lowercase anchored names, "born on" dates of birth, Commonwealth address formats, and an address false-positive fix.

Secrets pack (opt-in)

Catches leaked credentials before they reach a third-party model — useful when users paste config files or logs into AI features.

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

const sether = new Sether({
  detectors: [...basicDetectors, ...secretsDetectors],
});
ExportToken typeMethod
awsAccessKeyDetectorAWS_ACCESS_KEYAKIA-prefixed access key IDs.
openaiKeyDetectorOPENAI_KEYsk- / sk-proj- API keys.
anthropicKeyDetectorANTHROPIC_KEYsk-ant- API keys.
githubPatDetectorGITHUB_PATghp_ and github_pat_ tokens.
slackTokenDetectorSLACK_TOKENxox[baprs]- tokens.
stripeKeyDetectorSTRIPE_KEYsk_live / pk_live / test keys.
jwtDetectorJWTThree-part base64url tokens.
highEntropyDetectorHIGH_ENTROPYLong high-entropy strings (conservative).

0.7.0 also adds label-anchored detectors for API keys and passwords introduced in prose ("my api key is …", "password: …"), catching credentials that have no recognisable vendor prefix.

Alias engine (decoy mode)

A mask token tells the model something was hidden. A decoy does not. New in 0.7.0, the alias engine swaps a detected value for a realistic stand-in that reads naturally to an LLM, and an AliasVault maps every decoy back to the original. Restoration requires the matching retained vault and a recognizable returned alias.

Where a type has a reserved fictional or documentation range, built-in aliases use it: US phones in the 555-01XX block, UK Ofcom drama numbers, emails on example.com/org/net, IPv4 addresses from the RFC 5737 documentation blocks, SSNs from the SSA advertising range (987-65-43XX), and Luhn-valid cards in documented test BINs. Generated values are stand-ins, not encryption or a guarantee of anonymity; names and values without reserved ranges can coincide with real-world data.

Values with no reserved range (API keys, national IDs) get shape-preserving randomisation: length, character classes, and vendor prefixes (sk-, AKIA, ghp_) are kept, so the decoy still looks like the real thing. Exports: aliasValue, suggestAliases, shapeAlias, AliasVault.

Streaming & SSE

sether.redact() / sether.restore() are Node Transform streams — chunk-boundary safe, so a pattern split across chunks (foo@ + bar.com) is still caught. For LLM providers that stream Server-Sent Events, the SSE-aware variants redact/restore inside data: payloads while preserving framing:

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

providerResponse.body
  .pipe(createSSERestoreStream({ vault: sether.vault }))
  .pipe(clientResponse);

Lower-level building blocks: createRedactStream, createRestoreStream — same options, bring your own vault.

Middlewares

Drop-in wrappers so you don't hand-wire the streams:

import OpenAI from 'openai';
import Anthropic from '@anthropic-ai/sdk';
import {
  Sether, wrapFetch, wrapOpenAI, wrapAnthropic, createExpressMiddleware,
} from '@raeven-co/sether';

const sether = new Sether();
const opts = { detectors: sether.detectors, vault: sether.vault };

// fetch: redacts string request bodies, restores responses
const safeFetch = wrapFetch({ ...opts, fetchImpl: fetch });

// OpenAI / Anthropic SDKs: pass YOUR client instance. Sether never
// imports either SDK (structurally typed, zero peer dependencies)
const openai = wrapOpenAI(new OpenAI(), opts);
const anthropic = wrapAnthropic(new Anthropic(), opts);

// Express: redact inbound request bodies, restore outbound responses
app.use(createExpressMiddleware(opts));

JSON-safe restore (new in 0.8.0)

restoreSync, createRestoreStream and sether.restore() are plain text substitution: each original goes back exactly as it was. That is right for prose and wrong for serialized JSON, such as tool-call arguments, where an original containing a quote can add keys. Restore JSON value by value instead:

import { restoreJson, restoreJsonValue } from '@raeven-co/sether';

// Serialized JSON in, serialized JSON out: each string value is restored and re-escaped.
const safeArgs = restoreJson(toolCall.function.arguments, { vault, strict: true });

// Already parsed, e.g. an Anthropic tool_use block's `input`.
const input = restoreJsonValue(block.input, { vault, strict: true });
  • Object keys are never restored. Invalid JSON throws SyntaxError, with no fallback to text replacement.
  • strict: true (also accepted by restoreSync) throws UnresolvedTokenError when a token would be left in place: invented, expired, from another vault, case-altered or sitting in a key. The error lists tokens, never originals.
  • Since 0.8.0 the SSE stream, wrapFetch, the Express middleware and the SDK wrappers restore JSON value by value. Tool-call arguments stay tokenized unless you opt in with restoreToolArguments.
  • Strict mode is not authorization. Validate a tool call before you act on it.

Read the 0.8.0 release write-up →

Audit events & regulation mappings

The package ships the AuditEvent schema, two sinks, and default regulation references per detector (GDPR Art. 28, SOC 2 CC6.7, HIPAA, PCI DSS, EU AI Act, NDPA) in DEFAULT_REGULATION_MAPPINGS. An event records the detector, the token and the value's length, never the value. You write events where your app redacts; Sether does not log on its own.

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

const sink = new ConsoleAuditSink(); // JSONL to stderr; MemoryAuditSink for tests

sink.write({
  timestamp: new Date().toISOString(),
  detector: 'EMAIL',
  token: '<EMAIL_3f2a…>',
  valueLength: 17,
  action: 'REDACTED',
  destination: 'openai',
  regulationMappings: DEFAULT_REGULATION_MAPPINGS.EMAIL,
});

The events deliberately never contain the redacted value, so the log itself is evidence of data minimisation. Persistent storage, tamper-evident chaining and PDF export are on the hosted gateway roadmap.

The vault

The vault is the token → original-value map. Default: MemoryVault (in-process, LRU-bounded). It implements a six-method interface, set / get / has / delete / size / clear, so you can back it with Redis for multi-instance deployments. The vault stays inside your infrastructure; tokens are useless without it.

Browser entry

The package root uses Node streams and cannot be bundled for the browser. @raeven-co/sether/browser (0.5.0+) ships only the pure detection surface — every detector pack plus DEFAULT_REGULATION_MAPPINGS, no node: imports — and powers both the live sandbox and the Sether Shield extension:

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

const matches = [...basicDetectors, ...identityDetectors]
  .flatMap((d) => d.detect(text));

Free-text NER (@raeven-co/sether-ner)

Unlabelled names, organisations, and locations in running prose need a model, not a regex. That ships as a separate, lazy-loaded package so the core stays small. NER runs on the outbound prompt (sentence-buffered, async) — never on the streaming restore path — and mints the same <TYPE_…> tokens, so restore() works unchanged:

npm install @raeven-co/sether-ner @huggingface/transformers
import { Sether, redactSync, basicDetectors } from '@raeven-co/sether';
import { createNerRedactor } from '@raeven-co/sether-ner';

const sether = new Sether();
const ner = createNerRedactor(); // lazy-loads the model on first call

const { redacted } = await ner.redact(prompt, { vault: sether.vault });
const safe = redactSync(redacted, { detectors: basicDetectors, vault: sether.vault });
// send `safe` to the LLM — sether.restore() swaps BOTH token sets back

First call downloads the model (~30 MB) — call ner.warmup() at boot. Bring your own model via the infer option.

Sether Shield (browser extension)

The zero-code option for AI chat. Shield detects and replaces supported sensitive values in compatible prompt fields, locally. Free includes scanning, masks, tokens and restoration of all known mappings. New official licensed releases add Pro realistic decoys, custom watchlists and rules. Legacy 0.5.2 and earlier builds retain their legacy free features; the Pro pilot requires a compatible new build.

Detection, replacement and restore mappings stay on the device. New licensed clients make separate account/license activation and refresh requests to Sether using license metadata, without uploading prompts, replies, watchlist contents or mappings.

  • Use protected-site controls and the floating shield to review supported findings before sharing. Website interfaces can change; detection is not complete.
  • Free: mask a supported value or replace selected prompt text with a token. Optional auto-redact is off by default and runs after a two-second pause.
  • Pro in new releases: realistic decoys, an exact-term watchlist and custom detection rules. The full npm and PyPI libraries include these developer capabilities free under MIT.
  • Restore recognized masks, tokens and decoys whenever the matching session mapping exists, including after a Pro trial expires. A page refresh can retain mappings; closing the browser clears them.
  • AI Response Guard can restore an echoed known replacement on a supported reply interface. In-page restoration exposes originals to the website DOM; copying a restored reply is a separate clipboard action.
  • With pending findings, the first send pauses for review and a second send with the same findings proceeds. This is a review nudge, not a strict blocking mode.
  • Shortcut Alt+Shift+S scans on demand. The interface supports English, Spanish, French, German, Portuguese, Arabic and Simplified Chinese.

Shield edits page text; it is not a network proxy and does not filter the actual model request. Your chosen AI website receives what you submit. Durable mappings require a separately implemented vault; they are not provided by the browser session.

One-user Pro covers Shield and the editor: $5 for one month or $48 for one year, manually renewed. Install a compatible licensed build before explicitly starting the seven-day trial. No card, no automatic charge, and expiry returns to Free. Check current pilot availability →

VS Code extension

The same engine in your editor, live on the VS Code Marketplace: findings in the Problems panel, quick fixes for mask, redact, and decoy, plus copy-scrubbed and paste-restored for editor chat. Text processing stays on your machine. New licensed releases share the Shield Pro bundle; license requests contain metadata, not file contents. The editor source remains MIT licensed. Install the current Marketplace build with code --install-extension GodfreyLebo.sether.

See the walkthrough and install it →

Security

Found a vulnerability? Please don't open a public issue — email emorylebo@gmail.com or use GitHub private security advisories. We acknowledge within 48 hours. CI runs a safe-regex2 ReDoS scan (224 patterns, 0 unsafe) and an ASCII-only dist gate, and releases published from CI carry npm provenance attestation. Full policy: SECURITY.md.