Reversible redaction for calling any HTTP API on an untrusted provider without leaking PII or secrets.
Airlock wraps globalThis.fetch. Before any network call, sensitive values (emails, phones, CPF/CNPJ, API keys, cards, JWTs, private keys, AWS secrets, bearer tokens, …) are replaced with opaque, stable tokens like <<PII_EMAIL_1>>. The upstream provider only ever sees tokens. When the response comes back, Airlock restores the original values exactly — including mid-stream, byte-for-byte. Your code reads the real data; the provider never does.
It is universal: the fetch wrapper does not assume anything about the endpoint, the schema, or whether it's an LLM. Any fetch to any URL is covered. Schema-aware SDK sugar (@airlock/airlock/openai, @airlock/airlock/anthropic) is optional and ships on top.
- Zero runtime dependencies in the core engine.
- Isomorphic: Node 20+, browsers, Edge, Bun, Deno. Only standard Web APIs.
- No telemetry, no own network, no persisted state. The token↔original map lives in memory for the duration of a single fetch call and is dropped after.
- Untrusted: the upstream provider (arbitrary endpoint). It may log, train on, or resell your payload.
- Trusted: your process that imports
Airlock. - Redaction happens before the network. The provider sees only opaque tokens.
- Restoration happens in your process, after the response.
- The token↔original mapping is held in memory only, for the duration of the call. It is never persisted, logged, or sent anywhere.
- No telemetry, no phone-home, no external services.
npm install @airlock/airlockAirlock ships two packages: @airlock/core (the pure engine) and @airlock/fetch (the universal fetch wrapper). For most users the @airlock/airlock umbrella import is enough:
import { installRedactFetch } from '@airlock/airlock/fetch';Install the wrapper once at the start of your program. Every fetch() afterwards is redacted/restored automatically:
import { installRedactFetch } from '@airlock/airlock/fetch';
const uninstall = installRedactFetch({
// Which calls to redact. Default: all. Accepts a glob, an array of globs,
// or a predicate function. Omit to redact every fetch.
urls: ['https://upstream.example.com/**'],
// Which categories to redact. Default: DEFAULT_CATEGORIES.
// 'all' also enables the high-false-positive opt-in categories.
categories: 'all',
// What to touch on the request side.
request: {
body: true, // redact JSON/string/FormData/URLSearchParams/streaming bodies
urlQuery: true, // redact query-string values (path is never touched)
headers: false, // OFF by default; see "Headers" below
},
// Which response content-types to restore.
response: {
types: ['text/', 'application/json', 'text/event-stream'],
},
});
// Any fetch is now safe:
const r = await fetch('https://upstream.example.com/api/whatever', {
method: 'POST',
body: JSON.stringify({ user: { email: 'alice@example.com' } }),
});
// Upstream received: {"user":{"email":"<<PII_EMAIL_1>>"}}
// Your code sees the restored value:
const data = await r.json();
console.log(data.user.email); // 'alice@example.com'
// When you want to remove the wrapper:
uninstall();- JSON bodies are parsed and walked recursively. Every string value is redacted, regardless of where it sits in the schema. Unknown fields pass through untouched.
- Plain string bodies are redacted as text.
URLSearchParamsandFormDatahave their string values redacted;Blob/Fileentries pass through.ReadableStreamupload bodies are piped through a redactingTransformStream. Streaming uploads work.- Binary bodies (
ArrayBuffer, typed arrays, non-textBlob) are never touched. - URL query string values are redacted. The path is never modified.
Headers are not redacted by default. Authorization and custom auth headers are too easy to break. If you opt in, Airlock only redacts the values of headers you list explicitly — it never default-scans all headers:
installRedactFetch({
urls: 'https://up.example.com/**',
request: {
headers: ['x-customer-email'], // array form
// or: headers: { allow: ['x-customer-email'] },
},
});A single Restorer is created per fetch call (in a closure, sharing the request's token map). Restoration depends on the response content-type:
text/event-stream(SSE): the body is piped through a restoringTransformStream. Tokens split across chunk boundaries are reassembled via a safe lookahead;flush()finalizes at the end.application/jsonandtext/*: the body is buffered, restored, and theResponseis rebuilt with the same status, status text, and headers.- Non-text content-types (images, audio,
application/octet-stream, …): passed through untouched.
The wrapper preserves Response semantics — status, headers, trailers — and only swaps the body. gzip/brotli are decoded by the native fetch before Airlock sees the text.
Each fetch call gets its own Redactor + Restorer pair. There is no global state and no cross-talk between concurrent calls. Two parallel requests with different emails each restore their own email.
installRedactFetch() returns an uninstall function that restores the original fetch. Installing twice does not double-wrap; uninstalling restores the original.
Default categories are tuned for high sensitivity, low false-positive rate. Opt-in categories are useful but tend to match prose; enable them only when needed.
| Category | Token | Default | Opt-in | Notes |
|---|---|---|---|---|
email |
<<PII_EMAIL_n>> |
✓ | RFC-ish local-part@domain | |
phone |
<<PII_PHONE_n>> |
✓ | E.164 (+…) or (NN) area code; avoids swallowing CPF/card |
|
cpf |
<<PII_CPF_n>> |
✓ | Brazilian CPF, check-digit validated | |
cnpj |
<<PII_CNPJ_n>> |
✓ | Brazilian CNPJ, check-digit validated | |
apikey |
<<PII_APIKEY_n>> |
✓ | sk-…, sk_live_…, AKIA…, ghp_…, xox…, AIza…, api_key… |
|
card |
<<PII_CARD_n>> |
✓ | Credit card numbers, Luhn-validated | |
dburl |
<<PII_DBURL_n>> |
✓ | postgres://, mysql://, mongodb://, redis://, … |
|
jwt |
<<PII_JWT_n>> |
✓ | Three base64url segments, header validated | |
privatekey |
<<PII_PRIVATEKEY_n>> |
✓ | PEM -----BEGIN … PRIVATE KEY----- blocks |
|
aws |
<<PII_AWS_n>> |
✓ | 40-char AWS secret access keys | |
token |
<<PII_TOKEN_n>> |
✓ | Value after Bearer, token=, api_key=, Authorization: |
|
ip |
<<PII_IP_n>> |
✓ | IPv4 addresses | |
mac |
<<PII_MAC_n>> |
✓ | MAC addresses | |
cep |
<<PII_CEP_n>> |
✓ | Brazilian postal codes | |
pis |
<<PII_PIS_n>> |
✓ | Brazilian PIS/NIS, check-digit validated | |
ssn |
<<PII_SSN_n>> |
✓ | US SSN, SSA structural rules |
Non-overlap resolution: longer/more-specific patterns win. PEM private keys, database URLs, JWTs, and AWS secrets are matched before the generic apikey pattern so the canonical span is preserved. The same value always maps to the same token within one call.
If you don't want the fetch wrapper (e.g. you redact a log line, a prompt, a file), use @airlock/core:
import { Redactor } from '@airlock/airlock/core';
const r = new Redactor(); // default categories
const redacted = r.redact('email alice@example.com, cpf 529.982.247-25');
const rest = r.buildRestorer();
const back = rest.restoreAll(redacted); // exact original
// Streaming restore:
const s = r.buildRestorer();
let out = '';
for (const chunk of chunks) out += s.push(chunk);
out += s.flush();interface RedactFetchOptions {
urls?: string | string[] | ((url: string) => boolean);
categories?: readonly RedactCategory[] | 'all' | 'default';
request?: {
body?: boolean; // default true
urlQuery?: boolean; // default true
headers?: boolean | string[] | { allow?: string[] }; // default false
};
response?: {
types?: string[]; // default ['text/', 'application/json', 'text/event-stream']
};
}- You already fully control the provider and the transport (e.g. your own service over mTLS). Redaction adds overhead for no benefit.
- Your payload is meant to be opaque binary and you never send text PII.
Airlockwon't touch binary bodies anyway, but there's nothing to redact. - You need the provider to see the real PII to do its job (e.g. an email-sending API that needs the recipient address). Redacting defeats the purpose. Consider redacting only the fields the provider doesn't need.
- You require hard, cryptographic guarantees of non-disclosure.
Airlockis deterministic redaction, not encryption. If the provider must process the value, a token breaks that. If you need zero-knowledge processing, use a different architecture. - Opt-in categories (IP, MAC, CEP, PIS, SSN) on prose-heavy inputs. These match common shapes and will flag ordinary text. Prefer false-negatives over masking prose; leave them off unless your payload is structured.
| Airlock | server-side proxy | DLP gateways | field-masking SDKs | |
|---|---|---|---|---|
| Works with any HTTP API | ✓ | ✓ | ✓ | per-SDK |
| Redaction before network | ✓ | ✓ | ✓ | ✓ |
| Exact restoration in-process | ✓ | ✓ (proxy) | varies | ✓ |
| Streaming-safe (SSE) restore | ✓ | varies | varies | varies |
| No infra to run | ✓ | ✗ (run a proxy) | ✗ | ✓ |
| No provider trust needed | ✓ | ✓ | ✓ | ✓ |
| Zero deps / isomorphic core | ✓ | n/a | n/a | varies |
Airlock is library-first: no proxy to deploy, no gateway to route through. The airlock-proxy CLI companion exists for non-JS environments (curl, Python, Go, IDEs) and reuses the same core.
See SECURITY.md for the vulnerability disclosure policy, including how to report redaction bypasses.
MIT.