Open source · TypeScript · MIT
Keep sensitive text out of the model call.
privacy-core finds card numbers, emails and ID numbers in the text your users paste. It swaps each one for a label on the device, shows the exact text that will leave, and only sends after your code calls approve(). The real values come back in the reply, locally.
- 0.3.2 current release
- Node 22.13+ or a modern browser
- 1 runtime dependency
- 512 KiB input per call
The problem
The paste box is a data leak.
Add an “ask the AI about this” button to a banking, billing or support app, and people will paste whatever is on their screen: card numbers, account numbers, email addresses, sometimes a whole statement.
Send that text as written and all of it ends up with the AI company. Most teams either skip the feature or hope nobody pastes anything sensitive.
Read the threat model before deciding what you can put behind this.
Make the send a verb.
The detector proposes. redactForEgress() returns a reviewable PendingRedaction, and only approve() mints the RedactedPayload a provider accepts. A raw string does not type-check.
The audit sink is required, so an approval that nobody can observe is not an approval this boundary will grant.
What it does
Four steps. The send is the step your code cannot skip.
Detection is deterministic: fixed patterns, checksums and two small dictionaries. No model decides what is sensitive, and no network call is needed to find out.
-
01
Detect
Scans on the device for cards, emails, phones, SSNs, IBANs, labelled account and routing numbers, amounts, US dates, and a few demo names and merchants.
detect(text) -
02
Redact
Each recognized value goes into an in-memory
Vaultand is replaced by a stable, typed label such as[CARD_1]. The result is still only a proposal.redactForEgress(text, vault) -
03
Review & approve
A person sees the exact outgoing text. Only the explicit approval mints the sendable capability, and the audit sink is required.
approve(pending, audit) -
04
Restore
After the reply, the labels are swapped back from the same vault. The real values never crossed the wire.
rehydrate(reply.redactedText, …)
RedactedPayload. Passing a raw string is a compile error, and a hand-built look-alike is rejected at runtime by object identity.Coverage
It hides a list, not “everything private”.
The honest version of “it finds the private bits” is a list. If a format is not on it, it is not detected, and the human preview is the backstop.
- CARD
- PHONE
- SSN
- IBAN
- ROUTING
- ACCOUNT
- AMOUNT
- DATE
- NAME
- MERCHANT
Recognized in practice
- Luhn-valid card numbers in all the documented layouts.
- IBANs that pass mod-97, grouped or compact.
- Emails in any script; US/NANP phones in the documented formats.
- SSNs, labelled account and routing numbers, dollar amounts and US dates.
- Three demo names and five demo merchants. That is the whole name list.
- General names, addresses, free text, other currencies and other date formats.
Quickstart
Run the real loop in a minute.
Node 22.13 or newer, nothing else. The provider below is the built-in offline stand-in, so the whole loop runs with no API key and no network.
Create the project
mkdir try-privacy-core && cd try-privacy-core && npm init -y && npm install @gainratio/privacy-core
example.mjs
import { approve, guardedProvider, NoLLMProvider, redactForEgress, rehydrate, Vault } from "@gainratio/privacy-core";
const text = "Maria Lopez asked: refund $482.10 to card 4242 4242 4242 4242 and email maria@example.com.";
const vault = new Vault(); // keeps the real values, in memory, on this machine
const log = (entry) => console.log("[log]", entry.kind, entry.placeholders.join(" "));
const pending = await redactForEgress(text, vault, log); // 1. swap values for labels
console.log("will send: ", pending.redactedText);
const payload = approve(pending, log); // 2. a person has read the line above and said yes
const reply = await guardedProvider(new NoLLMProvider()).complete(payload); // 3. offline stand-in model
const answer = reply.redactedText.split("\n")[1];
console.log("AI saw: ", answer);
console.log("you see: ", rehydrate(answer, vault, payload.vaultRef)); // 4. real values put back locally
Run it
node example.mjs
Real output
[log] redact [AMOUNT_1] [CARD_1] [EMAIL_1]
will send: Maria Lopez asked: refund [AMOUNT_1] to card [CARD_1] and email [EMAIL_1].
[log] approve [AMOUNT_1] [CARD_1] [EMAIL_1]
AI saw: I reviewed your statement. It referenced 3 redacted value(s): [AMOUNT_1], [CARD_1], [EMAIL_1].
you see: I reviewed your statement. It referenced 3 redacted value(s): $482.10, 4242 4242 4242 4242, maria@example.com.
Limits
What it does not do.
Read this before trusting it with anything that matters. Over-claiming privacy is worse than claiming none.
It is not a name detector
General name detection is not shipped. The built-in list is three demo names, and it is only one entry in a fixed ruleset. The review step is the actual backstop.
It does not make text anonymous
The words around the labels can still point at one person: “$482.10, insurance, early January”. Redaction reduces direct identifier leakage; it is not anonymisation.
The vault is memory-only
Real values are held in process or tab memory and clear on reload. An encrypted stored vault is planned; it is not shipped.
It cannot protect a compromised device
A malicious extension, an XSS bug or a bad dependency can read the in-memory vault. Browser-held signing keys are protected by same-origin rules, not a secure element.
It does not control the provider
What the AI company does with the labelled text it receives is outside this library. Receipts record a decision; they do not constrain the receiver.
Input is capped
redactForEgress accepts at most 512 KiB of UTF-8 text and refuses larger input before detection. Split a large document into reviewed sections.
These are design boundaries, not bugs. The security page lists them in full.
Security & data handling
Where the data goes.
No account. No API key needed to try it. No network call after the install unless you call a provider.
On the device
Detection, the vault and rehydration run locally. Real values stay in memory for the session and are gone on reload. Nothing sensitive is persisted by the library.
On the wire
Only the approved placeholder text goes to the provider. Optional receipts contain a SHA-256 hash of that text, a decision and a provider name — never the text itself.
In your code
You pass the key and choose the provider; the library reads no environment variables and ships no server component. The only request is the provider call you configure.
FAQ
Questions worth asking.
Does it use AI to find sensitive values?
No. Detection is a fixed ruleset: regex patterns, checksum validators (Luhn, mod-97, SSA rules) and two small dictionaries. The same input produces the same spans, and detection works offline.
Does it work in the browser?
Yes. The package is ES modules with a browser-safe public barrel and is tested in Chromium. It also runs on Node 22.13 or newer. The demo is a Vite app in the repository.
What happens when the detector misses something?
It goes out as written unless the reviewer removes it. That is why the exact outgoing text is shown before approve(). Names, addresses and free text are outside the ruleset by design.
Can raw text ever be sent?
Only through unsafeBypass(raw, reason, audit), the explicit escape hatch. It always writes an audit entry. Every ordinary provider accepts only an approved RedactedPayload.
Does it store the real values?
Only in memory, for the life of the Vault. It clears on reload. A stored, encrypted vault is on the roadmap and is not part of this release.
Do I need signed receipts?
No. Receipts are opt-in: give guardedProvider a governance context and every allow or deny is signed into a hash-only record. Omit it and you get the same redaction and guard without receipts.
Which models can it call?
The bundled OpenRouterProvider speaks OpenAI-compatible chat/completions; makeProvider selects it when a key is present. You can also implement the LlmProvider contract yourself. NoLLMProvider is an offline echo for tests and demos.
How large can the input be?
512 KiB of UTF-8 per call. Larger input fails closed with InputTooLargeError before detection or vault writes. Split the document into reviewed sections.
Keep reading
Install it, then check the edges.
Documentation
Install, the API, how the placeholder mapping works, every typed error, and deployment guidance.
Examples
The README quickstart, detection on its own, the compile-time guard, signed receipts, and the offline demo.
Security
The threat model, what the tests prove, what they do not, supported versions and private reporting.
Source on GitHub · @gainratio/privacy-core on npm · GainRatio open source · SECURITY.md