peyeeye
v1.1.2
Published
Official TypeScript SDK for the peyeeye PII redaction API — detect, redact, and rehydrate personal data in LLM pipelines with deterministic tokens, zero retention, and stateless sealed sessions.
Maintainers
Keywords
Readme
peyeeye
Official TypeScript SDK for peyeeye.ai — redact PII on the way into your LLM prompts and rehydrate it on the way out. One round-trip, deterministic tokens, zero data retention by default.
- Works on Node 18+, Bun, Deno, Cloudflare Workers, and Vercel Edge.
- Zero runtime dependencies. Uses the platform
fetch. - Dual ESM + CJS build with typed
.d.ts/.d.cts. - Streaming (SSE) redact, stateless sealed mode, custom detectors — full parity with the HTTP API documented at https://peyeeye.ai/docs.
npm install peyeeyeGet an API key
- Sign up at https://peyeeye.ai/signup (free plan, no card required — 1 M characters / month, all 30+ built-in detectors).
- Head to https://peyeeye.ai/dashboard/keys → New key.
- Copy the full
pk_live_…(orpk_test_…) token shown once — only a hash is stored after you close the dialog. Drop it into your env:
export PEYEEYE_KEY=pk_live_...Test keys bypass billing and are rate-limited for development; live keys count against your plan. Paid tiers (Build / Pro / Scale) unlock streaming, custom detectors, and higher throughput — see https://peyeeye.ai/pricing.
Never ship an API key to the browser — call /redact and /rehydrate from
your server or edge runtime.
Quickstart
import { Peyeeye } from "peyeeye";
import Anthropic from "@anthropic-ai/sdk";
const peyeeye = new Peyeeye({ apiKey: process.env.PEYEEYE_KEY! });
const claude = new Anthropic();
const shield = await peyeeye.shield();
const safe = await shield.redact("Hi, I'm Ada, [email protected]");
const reply = await claude.messages.create({
model: "claude-sonnet-*",
max_tokens: 256,
messages: [{ role: "user", content: safe }],
});
console.log(await shield.rehydrate(reply.content[0].text));
// "Hi Ada, thanks — we've emailed [email protected]."shield() opens a session on first redact(), keeps using it across calls,
and swaps tokens back on rehydrate(). The same real value always yields the
same token inside one shield, and tokens never leak across shields.
Configuration
new Peyeeye({
apiKey: "pk_live_…",
baseUrl: "https://api.peyeeye.ai", // optional
maxRetries: 3, // default; 429 + 5xx back off exponentially
timeoutMs: 30_000, // default per-request timeout
defaultHeaders: { "X-App": "my-app" },
fetch: globalThis.fetch, // override e.g. for Cloudflare Workers
});All requests send Authorization: Bearer <apiKey>. Never ship the key to a
browser — proxy the redact + rehydrate calls from your backend.
Low-level calls
const r = await peyeeye.redact("Card: 4242 4242 4242 4242");
// r.redacted → "Card: [CARD_1]"
// r.session → "ses_…"
// r.entities → [{ token: "[CARD_1]", type: "CARD", span: [6, 25], confidence: 0.99 }]
const clean = await peyeeye.rehydrate("Confirmation for [CARD_1].", r.session);
// clean.text → "Confirmation for 4242 4242 4242 4242."Array input is processed in one session and mirrored on output:
const r = await peyeeye.redact(["Hi Ada", "email [email protected]"]);
// r.redacted[0] → "Hi [PERSON_1]"
// r.redacted[1] → "email [EMAIL_1]"Idempotency
await peyeeye.redact(text, { idempotencyKey: "req_a1b2c3" });Mismatched bodies with the same key raise idempotency_conflict (409). Same
body is served from the cache instantly.
Stateless sealed mode
Skip server-side storage entirely. The response includes a
rehydration_key (skey_…) — an AES-256-GCM-sealed blob of the token→value
mapping. Store it yourself, hand it back to rehydrate as the session:
const r = await peyeeye.redact("Email [email protected]", { session: "stateless" });
// r.rehydration_key → "skey_…"
const clean = await peyeeye.rehydrate("[EMAIL_1] received.", r.rehydration_key!);Or via a shield:
const shield = await peyeeye.shield({ stateless: true });
await shield.redact("Email [email protected]");
// shield.rehydrationKey holds the skey_… blob if you need to persist it
await shield.rehydrate("[EMAIL_1] received.");Streaming
redactStream() (SSE — Build plan and higher)
for await (const ev of peyeeye.redactStream(["Hi Ada", " card 4242 4242 4242 4242"])) {
if (ev.event === "session") console.log("session:", ev.data.session);
if (ev.event === "redacted") process.stdout.write(ev.data.text);
if (ev.event === "done") console.log("\ntotal chars:", ev.data.chars);
}Rehydrate an LLM token stream safely
Naive rehydration breaks when a chunk ends mid-token ("Hi [PERS"). The shield
buffers the partial token until the next chunk closes it:
const shield = await peyeeye.shield();
const safe = await shield.redact(userInput);
const upstream = await claude.messages.stream({
model: "claude-sonnet-*",
messages: [{ role: "user", content: safe }],
});
for await (const chunk of upstream) {
process.stdout.write(await shield.rehydrateChunk(chunk));
}
process.stdout.write(await shield.flush()); // emit any buffered tailNever call flush() while upstream is still delivering chunks — you can emit
a half-formed token.
Custom detectors
await peyeeye.createEntity({
id: "ORDER_ID",
kind: "regex",
pattern: "#A-\\d{6,}",
examples: ["#A-884217", "#A-007431"],
confidence_floor: 0.9,
});
// dry-run a pattern before saving
await peyeeye.testPattern({ pattern: "#A-\\d+", text: "#A-884217 and #A-1" });
// → { matches: [{ value: "#A-884217", start: 0, end: 9 }], count: 1 }
// list / update / retire
await peyeeye.listEntities();
await peyeeye.updateEntity("ORDER_ID", { enabled: false });
await peyeeye.deleteEntity("ORDER_ID");
// starter templates (Stripe keys, Twilio SIDs, JWTs, Slack tokens, …)
for (const t of await peyeeye.entityTemplates()) {
console.log(t.id, t.pattern);
}Plan gates: Free 0, Build 3, Pro 10, Scale unlimited. Over-cap returns
403 forbidden.
Sessions
await peyeeye.getSession("ses_…"); // → SessionInfo
await peyeeye.deleteSession("ses_…"); // drop immediately, don't wait for TTLFramework integrations
Vercel AI SDK
Drop-in middleware for wrapLanguageModel — redacts prompt text before the
model sees it, rehydrates tokens in the response, and buffers partial
placeholders across streamed chunks. One fresh session per model call, so
tokens never leak between requests.
import { wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
import { Peyeeye } from "peyeeye";
import { peyeeyeMiddleware } from "peyeeye/vercel-ai";
const peyeeye = new Peyeeye({ apiKey: process.env.PEYEEYE_KEY! });
const model = wrapLanguageModel({
model: openai("gpt-4o-mini"),
middleware: peyeeyeMiddleware({ peyeeye }),
});
// Use `model` anywhere the SDK expects a LanguageModel — generateText,
// streamText, generateObject — prompts are redacted in, responses rehydrated
// out, with zero extra app code.Opt into stateless sealed mode (no server-side mapping) with
peyeeyeMiddleware({ peyeeye, stateless: true }).
LangChain.js
Wrap any LangChain.js Runnable (chat model, LLM, or chain). Redacts the
prompt before .invoke(), rehydrates tokens in the response, and opens a
fresh session per call.
import { ChatOpenAI } from "@langchain/openai";
import { Peyeeye } from "peyeeye";
import { withPeyeeye } from "peyeeye/langchain";
const peyeeye = new Peyeeye({ apiKey: process.env.PEYEEYE_KEY! });
const model = withPeyeeye(new ChatOpenAI({ model: "gpt-4o-mini" }), { peyeeye });
const reply = await model.invoke("Hi, I'm Ada — [email protected]");Accepted inputs: plain strings, BaseMessage arrays, tuple shorthand
(["human", "text"]), dict messages ({ role, content }), and multimodal
content arrays — image parts pass through untouched. Exposes .invoke()
and .batch(). For LCEL composition (.pipe(...)) wrap with
RunnableLambda.from((x) => wrapped.invoke(x)).
Opt into stateless sealed mode with withPeyeeye(model, { peyeeye, stateless: true }).
Errors
Every non-2xx raises PeyeeyeError:
import { PeyeeyeError } from "peyeeye";
try {
await peyeeye.redact(input);
} catch (e) {
if (e instanceof PeyeeyeError) {
console.error(e.code, e.status, e.message, e.requestId, e.rateLimit);
if (e.retryable) { /* 429 / 5xx — SDK already retried up to maxRetries */ }
}
}Codes the backend uses: invalid_request, unknown_token, unauthorized,
forbidden, not_found, session_not_found, idempotency_conflict,
payload_too_large, rate_limited, internal_error.
Rate limits
Parsed from response headers and surfaced on PeyeeyeError.rateLimit:
{ limit: 500, remaining: 487, retryAfter: null }429s carry retryAfter in seconds — the SDK honours it automatically via
exponential backoff, capped at maxRetries.
Environment variables
The SDK itself reads no env vars. Typical usage:
PEYEEYE_KEY=pk_live_…new Peyeeye({ apiKey: process.env.PEYEEYE_KEY! });TypeScript types
Everything public is re-exported:
import type {
PeyeeyeOptions, RedactOptions, RedactResponse,
RehydrateOptions, RehydrateResponse,
DetectedEntity, SessionInfo, RateLimit,
EntitiesList, CustomDetector, EntityTemplate, StreamEvent,
} from "peyeeye";Using this SDK from an AI coding assistant
Copy-paste snippets — no fluff.
Install: npm install peyeeye
One round-trip through an LLM:
import { Peyeeye } from "peyeeye";
const peyeeye = new Peyeeye({ apiKey: process.env.PEYEEYE_KEY! });
const shield = await peyeeye.shield();
const safe = await shield.redact(userInput);
const reply = await callYourLLM(safe); // your own code
const out = await shield.rehydrate(reply); // tokens → real valuesStateless (no server-side storage):
const shield = await peyeeye.shield({ stateless: true });
const safe = await shield.redact(userInput);
// shield.rehydrationKey is the skey_… blob — persist it if you need to
const out = await shield.rehydrate(reply);Stream an LLM response back safely:
for await (const chunk of llmStream) {
process.stdout.write(await shield.rehydrateChunk(chunk));
}
process.stdout.write(await shield.flush());Register a custom detector:
await peyeeye.createEntity({
id: "ORDER_ID",
pattern: "#A-\\d{6,}",
examples: ["#A-884217"],
});Links
- Homepage: https://peyeeye.ai
- API reference: https://peyeeye.ai/docs
- Dashboard: https://peyeeye.ai/dashboard
License
MIT.
