openred
v0.2.0
Published
Fast, zero-dependency PII redaction for AI pipelines
Maintainers
Readme
openred
Fast, zero-dependency PII redaction for AI pipelines. Drop-in support for OpenAI, Anthropic, LangChain, Express, and Fastify.
Install
npm install openredQuick Start
import { RedactionPipeline } from 'openred';
const pipeline = new RedactionPipeline();
const result = pipeline.redact('Email [email protected], call 555-123-4567');
console.log(result.text);
// → "Email [EMAIL_1], call [PHONE_2]"
console.log(result.stats);
// → { totalDetected: 2, byType: { EMAIL: 1, PHONE: 1 }, processingTimeMs: 0.12 }What It Detects
| Type | Examples | Confidence |
|------|----------|------------|
| Email | [email protected], [email protected] | 0.95 |
| Phone | 555-123-4567, (555) 123-4567, +44 20 7946 0958 | 0.85–0.95 |
| SSN | 123-45-6789 (validates area/group/serial) | 0.90 |
| Credit Card | 4111-1111-1111-1111 (Luhn validated) | 0.80–0.95 |
| IP Address | 203.0.113.1 (skips private/localhost) | 0.90 |
| URL | https://example.com/path?q=search | 0.95 |
| Date of Birth | 01/15/1990, January 15, 1990 (context-aware) | 0.50–0.85 |
| Address | 123 Main Street (US street patterns) | 0.60–0.80 |
Strategies
const pipeline = new RedactionPipeline({ strategy: 'placeholder' });
// "Email [EMAIL_1], call [PHONE_2]" — numbered, deterministic tokens (default)
const pipeline = new RedactionPipeline({ strategy: 'category' });
// "Email [EMAIL], call [PHONE]" — simple type labels
const pipeline = new RedactionPipeline({ strategy: 'hash' });
// "Email [EMAIL:855f96e9], call [PHONE:a3c1d2e4]" — SHA-256 truncated
const pipeline = new RedactionPipeline({ strategy: 'mask' });
// "Email j███@e██████.com, call ███-███-4567" — partial reveal
// Custom strategy function
const pipeline = new RedactionPipeline({
strategy: (match) => `<<${match.type}>>`,
});Pipeline Configuration
import { RedactionPipeline, EmailDetector, PhoneDetector } from 'openred';
const pipeline = new RedactionPipeline({
// Pick specific detectors (default: all 8)
detectors: [EmailDetector, PhoneDetector],
// Redaction strategy (default: 'placeholder')
strategy: 'mask',
// Minimum confidence threshold (0-1, default: 0)
minConfidence: 0.7,
// Values to never redact
allowList: ['[email protected]'],
// How to resolve overlapping detections
overlapResolution: 'highest-confidence', // 'longest' | 'highest-confidence' | 'first'
// Enable vault for round-trip redaction
vault: true,
vaultTTL: 60000, // auto-expire entries after 60s
// Hooks for logging/audit
onDetection: (match) => console.log('Found:', match.type, match.value),
onRedaction: (result) => console.log('Redacted:', result.stats.totalDetected, 'items'),
});RedactionResult
Every call to pipeline.redact() returns a rich result object:
interface RedactionResult {
text: string; // redacted output
matches: PIIMatch[]; // all detected PII with positions and confidence
tokens: TokenMapping[]; // original → replacement mappings
stats: {
totalDetected: number;
byType: Record<string, number>;
processingTimeMs: number;
};
}Vault (Round-Trip Redaction)
Redact PII before sending to an LLM, then restore original values in the response.
const pipeline = new RedactionPipeline({ vault: true });
// Redact before sending to LLM
const result = pipeline.redact('My email is [email protected]');
// → "My email is [EMAIL_1]"
// LLM responds using the token
const llmResponse = 'I will contact you at [EMAIL_1].';
// Restore original values
const vault = pipeline.getVault();
const restored = vault.restore(llmResponse);
// → "I will contact you at [email protected]."
// Audit
vault.getEntries();
// → [{ original: '[email protected]', replacement: '[EMAIL_1]', type: 'EMAIL' }]
// Export/import vault state
const state = vault.export();
vault.import(state);LLM Integrations
OpenAI
import OpenAI from 'openai';
import { wrapOpenAI } from 'openred/integrations/openai';
const client = new OpenAI();
const safe = wrapOpenAI(client);
// Messages are redacted before the API call.
// Responses are automatically de-redacted.
const response = await safe.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: 'My email is [email protected]' }],
});
// response.choices[0].message.content has PII restored
// Access the pipeline for stats/config
safe.pipeline.getVault().getEntries();Set autoRestore: false to only redact outgoing messages without restoring responses:
const safe = wrapOpenAI(client, { autoRestore: false, strategy: 'category' });Anthropic
import Anthropic from '@anthropic-ai/sdk';
import { wrapAnthropic } from 'openred/integrations/anthropic';
const client = new Anthropic();
const safe = wrapAnthropic(client);
const response = await safe.messages.create({
model: 'claude-sonnet-4-5-20250929',
max_tokens: 1024,
messages: [{ role: 'user', content: 'My SSN is 123-45-6789' }],
});
// PII redacted before API call, restored in responseLangChain
import { ChatOpenAI } from '@langchain/openai';
import { wrapLangChain } from 'openred/integrations/langchain';
const llm = new ChatOpenAI({ model: 'gpt-4' });
const safe = wrapLangChain(llm);
const response = await safe.invoke('My phone is 555-123-4567');
// PII redacted before invoke, restored in responseMiddleware
Express
import express from 'express';
import { piiMiddleware } from 'openred/middleware/express';
const app = express();
app.use(express.json());
// Redact all strings in req.body
app.use(piiMiddleware({ strategy: 'placeholder' }));
// Or target specific fields
app.use(piiMiddleware({
fields: ['body.email', 'body.user.name'],
passVault: true, // attaches vault to req.openredVault
}));Fastify
import Fastify from 'fastify';
import { piiFastifyHook } from 'openred/middleware/fastify';
const app = Fastify();
app.addHook('preHandler', piiFastifyHook({ strategy: 'category' }));Locale Support
Built-in locale packs for region-specific PII patterns:
import { RedactionPipeline, enGB, deDE } from 'openred';
// UK: detects UK phone (+44), National Insurance Numbers, UK postcodes
const uk = new RedactionPipeline({ detectors: enGB });
// German: detects German phone (+49), Steuer-ID, PLZ (postal codes)
const de = new RedactionPipeline({ detectors: deDE });Custom Detectors
Add your own PII detectors with the plugin interface:
import { RedactionPipeline } from 'openred';
import type { PIIDetector } from 'openred';
const EmployeeIDDetector: PIIDetector = {
name: 'employee-id',
type: 'EMPLOYEE_ID', // extensible — any string works
confidence: 'high',
detect(text) {
const matches = [];
const re = /EMP-\d{6}/g;
let m;
while ((m = re.exec(text)) !== null) {
matches.push({
type: 'EMPLOYEE_ID' as const,
value: m[0],
start: m.index,
end: m.index + m[0].length,
confidence: 0.95,
detector: 'employee-id',
});
}
return matches;
},
};
const pipeline = new RedactionPipeline();
pipeline.addDetector(EmployeeIDDetector);
pipeline.redact('Contact EMP-123456 for details');
// → { text: 'Contact [EMPLOYEE_ID_1] for details', ... }Limitations
This package uses regex-based detection. It handles structured PII patterns well but cannot detect:
- Names — "Jordan" could be a name or a country
- Context-dependent PII — "My account number is 7483921" vs "there are 7483921 users"
- Implicit PII — "The CEO of Tesla" uniquely identifies a person
- Non-English formats — Limited to en-US, en-GB, and de-DE locales currently
For ML-powered contextual detection with higher accuracy, see the hosted API (coming soon).
License
MIT
