Node.js SDK /
Zero-dependency PII redaction for Node.js. 86KB package, ~1.6ms per page.
Installation
$ npm install euredactUpgrading from 0.6.0. Four changes in 0.7.0 can affect your code:
- Every credential is SECRET; CREDENTIAL is now a legacy alias that resolves to it. The Irish PPSN, the Austrian SVNr and the UK National Insurance number are SSN, not NATIONAL_ID.
- A span the cloud model found reports confidence null instead of "high"; filtering on "high" keeps checksum-backed rules detections only.
- A daily-quota 429 raises QuotaExceededError at once instead of after every retry.
- A cloud base URL that is not https is refused, except for localhost.
Which option do I need?
Start here when you know the task but not the option name. Every name is the camelCase form of the Python one.
| I want to… | Use |
|---|---|
| redact a document with no further setup | redact(text) |
| restrict scope to the countries I operate in | countries: ["NL", "BE"] |
| keep detection wide but resolve ambiguity | countryHint: ["DE"] |
| send a prompt to an LLM and restore the reply | tokenize: true, then restore() |
| keep relationships visible across a whole session | referentialIntegrity: true |
| never redact my own company name or addresses | allowlist: [...] |
| never redact anything at my own domain | allowlistDomains: ["acme.be"] |
| audit what the allowlist kept in the document | result.exempted |
| catch person names, employers, job titles, diagnoses | redactAsync(text, { mode: "cloud" }) |
| include dates of birth | detectDates: true |
| redact a document too large for one call | context + chunkOffset |
| detect an identifier the engine does not know | addCustomPattern() |
| isolate tenants from each other | one new EuRedact() each |
| free PII held in memory | clear() |
redact()
Main entry point. Redact PII from a text string. Returns a RedactResult with the cleaned text and a list of detections.
function redact(
text: string,
options?: RedactOptions
): RedactResult| Parameter | Type | Default | Description |
|---|---|---|---|
text | string | — | The input text to redact. |
countries | string[] | undefined | Country codes (e.g. ["NL", "BE"]) the document is declared to belong to. Since 0.3.2 this scores detection rather than gating it: every pattern runs whatever you declare, and a match attributed outside the set comes back with outOfScope true rather than being dropped. Declaring it is still worth 99.8% recall against 99.7% blind. Since 0.5.0 both SDKs score identically on every entity type, so this is the same trade-off the Python SDK makes. |
countryHint | string[] | undefined | A prior that resolves ambiguity without narrowing scope or flagging anything out of scope — use it when you know the likely origin but do not want outOfScope semantics. |
context | DocumentContext | undefined | Shares country evidence across the chunks of one document, so a chunk with no country signal of its own is still scored with what the rest of the document established. Pair with chunkOffset. Caching is disabled while a context is in use. |
chunkOffset | number | 0 | Where this chunk starts in the whole document. Used only to rebase spans recorded on a context; returned detections are always relative to the text passed in. |
mode | string | "rules" | "rules" runs locally and synchronously. "cloud" adds the model-only types and must go through redactAsync() — redact() is synchronous and a network call cannot be. |
referentialIntegrity | boolean | false | Replace PII with consistent labels (ENTITY_1, ENTITY_2, ...) instead of generic type labels. Labels persist on the instance across calls. Cannot be combined with tokenize. |
tokenize | boolean | false | Replace each value with a reversible token (EMAIL_K7Q2) and return the mapping in result.tokens, for restore(). Tokens are unique to the call, so nothing is retained. Cannot be combined with referentialIntegrity. |
allowlist | string[] | null | null | Values never to redact, whole-span and case-insensitive, merged with the instance's list. Structured identifiers also match across spacing and hyphenation. Applies to cloud-tier types too. |
allowlistDomains | string[] | null | null | Domains whose addresses are never redacted, e.g. ["acme.be"]. EMAIL and URL only, subdomains covered, matched on a label boundary — so acme.be does not exempt evilacme.be. |
detectDates | boolean | false | Include DOB and date-of-death detections. Off by default. |
cache | boolean | true | Enable result caching for faster subsequent calls. |
Return value
A RedactResult object with:
| Parameter | Type | Default | Description |
|---|---|---|---|
redactedText | string | — | The text with PII replaced by labels like [NATIONAL_ID], [BANK_ACCOUNT], etc. |
detections | Detection[] | — | Array of detected PII entities with type, value, position, and country. |
source | string | — | Source identifier for the redaction engine. |
degraded | boolean | — | Whether the result was produced in a degraded mode. |
inferredCountries | [string, number][] | — | New in 0.3.2. The countries the engine concluded the document belongs to, each with a score in 0-1, most likely first. Populated whether or not you passed countries. |
evidence | CountryEvidence[] | — | New in 0.3.2. The individual signals behind inferredCountries, so an attribution can be audited rather than taken on faith. |
tokens | Record<string, string> | — | Token to original value. Populated only with tokenize: true, and the only copy that exists — pass it to restore(), and drop it when you no longer need the values back. |
exempted | Exemption[] | — | Every span the allowlist kept, with the rule that matched and whether it was a value or a domain rule. An exemption leaves a direct identifier in a document that otherwise claims to be redacted, so it is reported rather than silent. |
detectionMode | "declared" | "inferred" | — | New in 0.3.2. "declared" when you passed countries, "inferred" otherwise (including when you passed only countryHint). |
Detection
Each detection in the array contains:
| Parameter | Type | Default | Description |
|---|---|---|---|
entityType | EntityType | — | The type of PII detected (enum value). |
start | number | — | Start character index in the original text. |
end | number | — | End character index in the original text. |
text | string | — | The original PII text that was detected. |
source | DetectionSource | — | The detection engine that found this entity. |
country | string | null | — | The country code associated with this detection, or null. |
confidence | string | — | The verdict on this identifier. "high" means a checksum or an unambiguous format confirmed it; "low" means the span was recognised and not confirmed — typically a labelled identifier whose check digit failed, which is masked anyway rather than printed in full. Filter on this, not on countryConfidence, when you need confirmed identifiers only: const strict = result.detections.filter((d) => d.confidence === "high" && !d.outOfScope). |
countryConfidence | number | — | New in 0.3.2. How strongly the document supports the country on this detection, 0-1. Separate from confidence, which is about the match itself: a pattern can fire unambiguously while its country attribution stays a guess. |
outOfScope | boolean | — | New in 0.3.2. True when the detection was attributed to a country outside the countries you declared. It is still redacted and still returned — filter on this field if you only want in-scope entities. |
CountryEvidence
One signal supporting a country attribution. New in 0.3.2.
| Parameter | Type | Default | Description |
|---|---|---|---|
country | string | — | The ISO country code this signal points at. |
source | string | — | What produced the signal, e.g. "ibanPrefix", "phonePrefix", "tld". |
logOdds | number | — | How much this signal moves the score. Signals accumulate, so several weak ones can outweigh a single strong one. |
span | [number, number] | — | Where in the text the signal was found. Offsets are absolute when chunkOffset is passed. |
DocumentContext
New in 0.3.2. Country inference reads signals out of the text, so a document processed in chunks loses accuracy at exactly the chunks that carry no signal of their own — a page of bare reference numbers between two pages full of Dutch addresses. A DocumentContext carries the evidence across chunks so the whole document is scored as one.
import { DocumentContext, redact } from "euredact";
const ctx = new DocumentContext();
let offset = 0;
for (const chunk of chunks) {
const result = redact(chunk, { context: ctx, chunkOffset: offset });
offset += chunk.length;
console.log(result.redactedText);
}
// Every signal the document produced, spans pointing into the full text.
console.log(ctx.evidence);Pass chunkOffset or the evidence spans will all point into the start of the text. Caching is disabled while a context is in use, since the result depends on the document rather than on the chunk alone.
In 0.3.3 evidence became a getter, not a method — it was a method alongside size, a getter, on the same class. Read it as ctx.evidence; ctx.evidence() now throws. The Python SDK keeps ctx.evidence(), following its own idiom.
Reuse a context only across chunks of the same document. A context shared between unrelated documents mixes their countries, which cannot cause a miss — a context influences scoring only, never which spans are found — but can attribute a value to the wrong national scheme.
Example
import { redact } from "euredact";
const result = redact(
"Mijn BSN is 111222333 en IBAN NL91ABNA0417164300.",
{ countries: ["NL"] }
);
console.log(result.redactedText);
// "Mijn BSN is [NATIONAL_ID] en IBAN [BANK_ACCOUNT]."
console.log(result.detections);countries takes an array. Since 0.3.3 a bare string throws a TypeError, on every entry point, in both SDKs. Before that, countries: "NL" was walked character by character into the codes "N" and "L"; neither resolves, so the call declared nothing and every detection came back with outOfScope true while the redacted text still looked correct. A pipeline filtering on that field — which these docs tell you to do — kept none of them. A wrong country code still only warns; a wrong type has no correct reading to fall back on.
redactBatch()
Process multiple texts efficiently. Loads country configs once. Same options as redact(), returns an array of RedactResult.
function redactBatch(
texts: string[],
options?: RedactOptions
): RedactResult[]Example
import { redactBatch } from "euredact";
const results = redactBatch([
"BSN 111222333",
"IBAN DE89370400440532013000",
]);
results.forEach((r) => {
console.log(r.redactedText);
});addCustomPattern()
Register a custom regex pattern at runtime. Detected matches will be labeled with the given entity type.
function addCustomPattern(
entityType: string,
pattern: string
): void| Parameter | Type | Default | Description |
|---|---|---|---|
entityType | string | — | The label to use for matches (e.g. "EMPLOYEE_ID"). |
pattern | string | — | A regex pattern string to match against input text. |
Example
import { addCustomPattern, redact } from "euredact";
addCustomPattern("EMPLOYEE_ID", "EMP-\\d{6}");
const result = redact("Contact EMP-123456 for details");
console.log(result.redactedText);
// "Contact [EMPLOYEE_ID] for details"availableCountries()
Returns an array of supported ISO country codes.
import { availableCountries } from "euredact";
console.log(availableCountries()); // ["AT", "BE", "BG", ...]restore()
function restore(text: string, tokens: Record<string, string>): stringPut the original values back into text that derives from a tokenize: trueresult — typically a model’s reply to the tokenized prompt. tokens is result.tokens. Every occurrence is replaced, including a token a model has glued to other characters (EMAIL_P4RTs) — leaving a token behind is the worse failure.
Reversible tokenization
tokenize: true is for text that has to come back — a prompt you send to a model, whose reply has to read as if the values were never gone.
import { redact, restore } from "euredact";
const result = redact(prompt, { countries: ["BE"], tokenize: true });
console.log(result.redactedText);
// "Write an email to Joren at EMAIL_K7Q2 or call PHONE_P4RT about the invoice."
const reply = await callYourLlm(result.redactedText);
console.log(restore(reply, result.tokens));Within one call the same value gets the same token, so a prompt that names someone twice still reads as one person. Across calls it gets a different one: nothing is retained on the instance, result.tokens is the only copy, and two tokenized documents never reveal that they share a value. That is the opposite retention model from referentialIntegrity, which is why the two cannot be combined. A token is TYPE_ plus four characters from an alphabet with no vowels and no 0/1/I/O.
Allowlist
Values a caller declares are not PII to them— their own organisation’s name, their own addresses. Set it per call, on the instance, or both; the two merge. It applies to cloud-tier types as well.
const sdk = new EuRedact({ allowlistDomains: ["acme.be"] });
sdk.redact("Mail jan@acme.be or piet@acme.be", { countries: ["NL"] }).redactedText;
// "Mail jan@acme.be or piet@acme.be"A structured identifier matches however the document spaces it, so an allowlisted IBAN is exempt whether it is written NL91ABNA0417164300 or NL91 ABNA 0417 1643 00. Free-text types stay literal, because folding would exempt values the caller never listed. Domain rules cover EMAIL and URL only, including subdomains, and match on a label boundary — so acme.be does not exempt evilacme.be. Every exemption is reported in result.exempted, with the rule that matched, so a document that deliberately keeps a direct identifier can be audited.
There are deliberately no wildcards. The allowlist is the only option that turns redaction off, so an over-broad entry fails toward under-redaction and does so silently.
Cloud tier
The cloud tier is in closed alpha. The options below are documented because the SDK accepts them; access is by invitation while the service is in closed testing.
mode: "cloud" adds the types no pattern can match — person names, organisations, job titles, diagnoses. It must go through redactAsync(): redact() is synchronous and a network call cannot be. This is the one structural difference between the two SDKs; everything else is the camelCase form of the same option.
const result = await redactAsync(text, { countries: ["BE"], mode: "cloud" });Credentials come from configure({ apiKey, baseUrl }) or from EUREDACT_API_KEY and EUREDACT_BASE_URL. TypeScript has no coref option; the Python SDK accepts one as a reserved, unimplemented argument.
Secret Detection
euRedact automatically detects secrets and API keys using two strategies: known-prefix patterns for popular services (AWS, GitHub, Stripe, OpenAI, Slack, JWT, SendGrid) and an entropy-based fallback that catches generic secrets near context keywords like api_key, token, and secret. Detected secrets are labeled as [SECRET].
import { redact } from "euredact";
const result = redact(
"My API key is sk-proj-abc123def456ghi789"
);
console.log(result.redactedText);
// "My API key is [SECRET]"Performance
Supported Countries
31 European and EEA countries with country-specific PII patterns.
Entity Types
These are the 29 types this package emits, across all supported countries.
27 is the Rules Engine layer. The forthcoming AI layer adds 13 more — names, addresses, medical information and the other categories that have no fixed format for a pattern to match, plus the GDPR Article 9 special-category types. What euRedact detects lists all 40 with the layer responsible for each, so a count of 40 elsewhere on this site is not a different number from this one.
View source on GitHub
Browse the code, report issues, or contribute.