{"slug": "flare-redact-scoped-secret-redaction-for-javascript-ai-agents", "title": "Flare Redact – scoped secret redaction for JavaScript AI agents", "summary": "Flare Redact, a new open-source JavaScript library, provides scoped secret and PII redaction for AI agents, detecting and masking sensitive data in logs, prompts, and text across 24 languages with zero runtime dependencies. The library, available via npm as flare-redact 1.0, offers context-aware detection, secure vaults, and middleware for wrapping any SDK, including LLM clients, to strip secrets from prompts before they reach models. It aims to prevent data leaks from logging and AI interactions, supporting Node 20+, browsers, and edge runtimes.", "body_md": "**Hide secrets & PII in logs, prompts, and text — before they leak.**\n\n🌐 **International by default — 24 languages**\n\n🇬🇧 🇨🇳 🇮🇳 🇪🇸 🇸🇦 🇫🇷 🇵🇹 🇷🇺 🇯🇵 🇩🇪 🇰🇷 🇹🇷 🇮🇹 🇮🇷 🇵🇱 🇺🇦 🇳🇱 🇻🇳 🇮🇩 🇹🇭 🇬🇷 🇮🇱 🇦🇿 🇷🇴\n\n[Live playground](https://flare-collection.github.io/flare-redact/)\n· [Practical redaction guides](https://flare-collection.github.io/flare-redact/guides/)\n· [LLM-friendly API reference](https://flare-collection.github.io/flare-redact/llms-full.txt)\n\nEvery leaked secret has the same origin story: someone logged an object, and a\npassword, token, or API key was sitting inside it. The code looked innocent —\n`logger.info({ user })`\n\n— but `user`\n\ncarried a session token, and now it's in\nyour log aggregator, your error tracker, and three vendors' systems forever.\n\n**flare-redact** is one function you wrap around that data. It reads the *content*,\nnot just the field names, so it catches the AWS key someone pasted into a free-text\n`note`\n\n, the JWT in an `Authorization`\n\nheader, the card number in a stack trace — and\nmasks them, keeping just enough of a hint to stay debuggable.\n\n``` js\nimport { redact } from 'flare-redact';\n\nredact('User alice@corp.com paid with 4242 4242 4242 4242, token ghp_' + 'a'.repeat(36));\n// → 'User a***@*** paid with **** **** **** 4242, token ghp_***'\n```\n\nNothing to configure. No list of field paths to maintain. No native build step.\n\nThe same problem now has a new address: your LLM calls.Wrap your OpenAI or Anthropic client and detected secrets are stripped from prompts and restored in the reply — the model never sees those original values, while references survive.[Jump to it ↓]\n\n🔍 Context-aware — spans carry risk and confidence |\n🔐 Secure vaults — opaque tokens, optional AES-GCM persistence |\n🎭 Useful test data — keyed pseudonyms and typed surrogates |\n🤖 Scoped LLM + tool boundaries — stops cross-tool placeholder restore |\n🔌 Universal middleware — wrap any SDK, handler, queue, or RPC function |\n🪶 Zero runtime dependencies — Node, browser, and edge |\n\n[Install](#install)[Practical guides](https://flare-collection.github.io/flare-redact/guides/)[Runnable examples](#runnable-examples)[Redact anything](#redact-anything)[Integrate any SDK or framework](#integrate-any-sdk-or-framework)[Redact prompts before they reach an LLM](#redact-prompts-before-they-reach-an-llm)[Ways to hide a value](#ways-to-hide-a-value)[Reversible redaction](#reversible-redaction)[Contextual and model-assisted PII](#contextual-and-model-assisted-pii)[Learned confidence, fewer false positives](#learned-confidence-fewer-false-positives)[Build a private chat app](#build-a-private-chat-app)[Protect tool calls and MCP loops](#protect-tool-calls-and-mcp-loops)[Your own words](#your-own-words)[See what leaks, and why](#see-what-leaks-and-why)[Guard your logger in one line](#guard-your-logger-in-one-line)[One policy, everywhere](#one-policy-everywhere)[Anonymize a dataset for staging](#anonymize-a-dataset-for-staging)[Guard what leaves your app](#guard-what-leaves-your-app)[Streams](#streams)[Fail a build when a secret sneaks in](#fail-a-build-when-a-secret-sneaks-in)[CLI](#cli)[What it catches](#what-it-catches)[Multilingual secret vocabulary and IDs](#multilingual-secret-vocabulary-and-ids)[Custom detectors & allowlists](#custom-detectors--allowlists)[API](#api)[Security boundaries](#security-boundaries)[Why not a field allowlist?](#why-not-a-field-allowlist)\n\n```\nnpm install flare-redact\n```\n\nNode 20+, and it runs in the browser and edge runtimes too — zero dependencies.\nUpgrading from `0.9.x`\n\n? Read the [ 1.0 migration guide](/flare-collection/flare-redact/blob/main/MIGRATION.md). Existing\nprojects are not forced across the major version; upgrade explicitly with\n\n`npm install flare-redact@^1.0.0`\n\n.The core is plain ESM with no Node built-ins, is tree-shakeable\n(`sideEffects: false`\n\n), and uses the standard Web Crypto API — so `redact`\n\n,\n`scan`\n\n, vaults, and the LLM helpers work unchanged in React, Vue, and edge\nfunctions. CI smoke-tests the core on Bun and Deno on every push.\n\n``` js\nimport { redact } from 'flare-redact';\n\nfunction SupportTicket({ text }) {\n  // Mask pasted keys and card numbers before the ticket is rendered or sent on.\n  return <pre>{redact(text)}</pre>;\n}\n```\n\nOnly the Node-specific entry points (`flare-redact`\n\nCLI, `/stream`\n\n, `/pino`\n\n,\n`/winston`\n\n) need Node. One honest caveat: client-side redaction protects what\nyou *forward* (analytics, logs, LLM calls) — it is not a substitute for\nserver-side redaction, since the original value already reached the browser.\n\nThe docs site serves an [ llms.txt](https://flare-collection.github.io/flare-redact/llms.txt)\nand a condensed\n\n[API reference, so coding assistants that read documentation get the current API instead of guessing. If you use Claude Code, Cursor, or similar agents, one line in your project rules (](https://flare-collection.github.io/flare-redact/llms-full.txt)\n\n`llms-full.txt`\n\n`CLAUDE.md`\n\n, `.cursor/rules`\n\n, `AGENTS.md`\n\n)\nkeeps generated code consistent:\n\n```\nFor masking secrets/PII in logs, prompts, or datasets, use the flare-redact\npackage (API: https://flare-collection.github.io/flare-redact/llms-full.txt).\nDo not write ad-hoc redaction regexes.\n```\n\nClone the repository and run these small applications locally:\n\n| Example | What it proves | Run |\n|---|---|---|\n`openai-privacy` |\n\n`npm --prefix examples/openai-privacy start`\n\n`express-pino`\n\n`npm --prefix examples/express-pino run smoke`\n\n`universal-boundaries`\n\n`npm --prefix examples/universal-boundaries start`\n\n`github-secret-scan`\n\nRun `npm run build`\n\nand install an example's dependencies before its first run.\n\nStrings, arrays, and objects, recursively. The shape you pass in is the shape you get back.\n\n``` js\nimport { redact } from 'flare-redact';\n\nredact({\n  user:     'bob@corp.com',\n  password: 'hunter2',\n  tokens:   ['ghp_' + 'b'.repeat(36)],\n  note:     'my aws key is AKIAIOSFODNN7EXAMPLE',\n});\n// →\n// {\n//   user:     'b***@***',\n//   password: '***',                     // sensitive field name\n//   tokens:   ['ghp_***'],\n//   note:     'my aws key is AKIA***',    // found inside free text\n// }\n```\n\n`flare-redact/middleware`\n\nputs one compiled policy around plain-data function\nboundaries. It works without framework dependencies: analytics SDK methods,\nserver-action payloads, queue consumers, webhook clients, RPC functions,\ndatabase writes, and telemetry exporters all use the same boundary shape.\n\n``` js\nimport { createRedactionMiddleware } from 'flare-redact/middleware';\n\nconst boundary = createRedactionMiddleware({\n  policy: {\n    enable: ['high_entropy'],\n    refineConfidence: true,\n    minConfidence: 0.65,\n  },\n  onFindings(event) {\n    audit.info({\n      boundary: event.name,\n      count: event.findings.length,\n      detectors: event.findings.map((finding) => finding.detector),\n    }); // finding metadata is always value-free\n  },\n});\n\nanalytics.track = boundary.wrap(analytics.track, { name: 'analytics.track' });\nawait analytics.track('checkout', { email, authorization });\n// analytics receives a redacted copy; the caller's object stays unchanged\n```\n\nThe same boundary supports three deployment modes:\n\n```\ncreateRedactionMiddleware({ action: 'redact' });  // default: sanitize and continue\ncreateRedactionMiddleware({ action: 'observe' }); // report only, change nothing\ncreateRedactionMiddleware({ action: 'block' });   // throw RedactionBlockedError\n```\n\nUse `process(value)`\n\ndirectly inside framework hooks. `wrap()`\n\nhandles sync and\npromise-returning functions while preserving method `this`\n\n; `wrapAsync()`\n\nadds\nasync local `semanticProvider`\n\nsupport. Inputs are protected by default, and\n`{ output: true }`\n\nalso protects returned values. For signatures containing\nframework-native request/response objects or callbacks, protect only plain-data\narguments (`{ input: [1] }`\n\n) or call `process(request.body)`\n\nexplicitly; do not\nclone a native `Request`\n\n/`Response`\n\nobject.\n\nYour app sends user data to OpenAI or Anthropic. Somewhere in that prompt is a customer's email, an API key, or a card number — and now it's left your systems. Wrap the client once, and detected secrets are stripped from prompts and put back in the reply. The model never sees those original values; your code keeps the references it needs.\n\n``` js\nimport { wrapOpenAI } from 'flare-redact/llm';\n\nconst openai = wrapOpenAI(new OpenAI());\n\nconst res = await openai.chat.completions.create({\n  model: 'gpt-4o',\n  messages: [{ role: 'user', content: 'Email the invoice to alice@corp.com, card 4242 4242 4242 4242' }],\n});\nyour app sends  →  Email the invoice to alice@corp.com, card 4242 4242 4242 4242\nthe model sees  →  Email the invoice to [FR_EMAIL_7f2a…], card [FR_CREDIT_CARD_19be…]\nyour app gets   →  Sent to alice@corp.com. Card 4242 4242 4242 4242 wasn't stored.\n```\n\n`wrapAnthropic`\n\ndoes the same for `messages.create`\n\n, including the system prompt.\nBoth wrappers redact complete message structures, including tool-call arguments.\nStreaming text, OpenAI tool arguments, and Anthropic partial JSON are restored\neven when a placeholder is split across chunks. There's a `redactPrompt(text)`\n\ntoo if you'd rather hold the vault yourself.\n\nPick a `mode`\n\ndepending on whether you still need to *reason* about the data\nafter it's hidden.\n\n```\nredact('bob@corp.com', { mode: 'mask'  }); // 'b***@***'          (default)\nredact('bob@corp.com', { mode: 'label' }); // '[REDACTED:email]'\n\nconst protectedOptions = { transformSecret: process.env.FLARE_REDACT_SECRET };\nredact('bob@corp.com', { ...protectedOptions, mode: 'hash' });\n// 'email_3baf4d28d7c88317a…' — HMAC-SHA-256 fingerprint\n\nredact('bob@corp.com', { ...protectedOptions, mode: 'pseudonym' });\n// 'kqz@rwmp.dnu' — keyed, deterministic, keeps character classes\n\nredact('bob@corp.com', { ...protectedOptions, mode: 'surrogate' });\n// 'user_93a78c61e204@example.invalid' — type-consistent synthetic value\n```\n\nProtected deterministic modes require `transformSecret`\n\n; they never silently\nfall back to a public unsalted fingerprint. `hash`\n\nis useful for correlation,\n`pseudonym`\n\nretains the original character shape, and `surrogate`\n\nemits typed\nsynthetic values such as reserved-domain emails and Luhn-valid card numbers.\nUse a separate secret per environment or correlation domain.\n\n`pseudonym`\n\nis deliberately **not** described as format-preserving encryption.\nIt is non-reversible pseudonymization, not NIST FF1. The old `fpe`\n\nname remains\nas a compatibility alias but is deprecated.\n\nOr replace everything with one fixed string:\n\n``` js\nredact(payload, { mask: '█' });\nredact(payload, { mask: ({ detector }) => `<${detector.id}>` });\n```\n\nWhen you need the originals back — the LLM case above, or handing data to a system you don't trust and getting it back — use a vault. It swaps each secret for a stable placeholder and remembers the mapping.\n\n``` js\nimport { createVault } from 'flare-redact';\n\nconst vault = createVault();\nconst safe = vault.redact('charge bob@corp.com on card 4242 4242 4242 4242');\n// 'charge [FR_EMAIL_7f2ad4…] on card [FR_CREDIT_CARD_19be63…]'\n\nvault.restore(safe);\n// 'charge bob@corp.com on card 4242 4242 4242 4242'\n```\n\nThe same value gets the same placeholder inside one vault, so references survive\nthe round trip. Default placeholders include 96 random bits instead of a global\nsequence number. Human-readable `[EMAIL_1]`\n\ncounters remain available through\n`createVault({ placeholderStyle: 'readable' })`\n\nfor trusted local workflows.\n\nThe mapping is as sensitive as the original data. Encrypt it before persistence:\n\n``` js\nimport { sealVault, openVault, restore } from 'flare-redact';\n\nconst encrypted = await sealVault(vault, process.env.FLARE_REDACT_VAULT_PASSWORD);\nawait fs.writeFile('session.vault.json', JSON.stringify(encrypted), { mode: 0o600 });\n\nconst entries = await openVault(encrypted, process.env.FLARE_REDACT_VAULT_PASSWORD);\nrestore(safe, new Map(entries));\n```\n\nSealed vaults use PBKDF2-SHA-256 with a fresh salt and AES-256-GCM with a fresh nonce. Wrong passwords and modified files fail closed.\n\nFrom the CLI, `--vault`\n\nand `--restore`\n\nuse encrypted files by default. Passwords\ncome from `FLARE_REDACT_VAULT_PASSWORD`\n\n(or the variable named by\n`--vault-password-env`\n\n) so they do not appear in shell history:\n\n```\nexport FLARE_REDACT_VAULT_PASSWORD='use-a-secret-manager-in-production'\nflare-redact --vault session.vault.json < input.txt > safe.txt\nflare-redact --restore session.vault.json < safe.txt > restored.txt\n```\n\nStructured identifiers and credentials are best handled by deterministic rules and checksum validators. Names and addresses need context, so three conservative detectors are opt-in:\n\n``` js\nconst findings = scan(\n  'Customer name: Alice Example; address: 120 Cedar Street; DOB: 1990-04-23',\n  { enable: ['contextual'] },\n);\n\n// person_name, street_address, date_of_birth\n// each finding includes risk, confidence, and the exact sensitive span\n```\n\nFor broader multilingual free-text PII, connect a local model without coupling the zero-dependency core to one ML runtime:\n\n``` js\nconst policy = {\n  semanticProvider: {\n    async detect(text) {\n      return [{\n        detector: 'person_model', label: 'Person',\n        why: 'Local multilingual NER result.',\n        start: 12, end: 25, confidence: 0.94, risk: 'high',\n      }];\n    },\n  },\n  minConfidence: 0.8,\n};\n\nconst safe = await redactAsync(input, policy);\n```\n\nSemantic and deterministic spans enter the same overlap arbitration. Higher-risk, higher-priority, and better-validated findings win instead of whichever regular expression happens to run first.\n\nGeneric, format-agnostic detectors such as `high_entropy`\n\ncatch unknown-format\nkeys, but they also fire on benign high-entropy strings: UUIDs, git SHAs, digests,\nobject ids, and slugs. `refineConfidence`\n\nruns a small learned classifier over\neach match to tell real secrets from look-alikes, then nudges the confidence\nscore. Pair it with `minConfidence`\n\nto drop the noise.\n\n``` js\nconst noisy = 'id 9fceb02d0ae598e95dc970b74767f19372d61af8 tok Zx9Kq2Lm7Pv4Rt6Wy8Bn3Cf5Hj1Dg0As7Uv';\n\nscan(noisy, { enable: ['high_entropy'] });\n// git SHA and the unknown-format token both flagged at a flat 60%\n\nscan(noisy, { enable: ['high_entropy'], refineConfidence: true, minConfidence: 0.5 });\n// the SHA is gone; the token survives (refined up to 80%)\n```\n\nThe classifier is logistic regression over cheap character features (entropy,\ncharacter-class mix, structure, and nearby labels like `api_key=`\n\nor `commit`\n\n).\nIt is trained offline by [ scripts/train-confidence-model.mjs](/flare-collection/flare-redact/blob/main/scripts/train-confidence-model.mjs)\nand shipped as fixed weights, so the runtime stays zero-dependency, synchronous,\nand deterministic — no model download, no native add-on, safe on edge and in the\nbrowser. Only detectors marked\n\n`refine`\n\nare touched; checksum-validated ones\n(cards, IBANs, national ids) are always left alone.Score a string yourself from `flare-redact`\n\nor the `flare-redact/ml`\n\nsubpath:\n\n``` js\nimport { secretProbability } from 'flare-redact/ml';\n\nsecretProbability('Zx9Kq2Lm7Pv4Rt6Wy8Bn3Cf5Hj1Dg0As7Uv', 'authorization: Bearer …'); // ~1.00\nsecretProbability('9fceb02d0ae598e95dc970b74767f19372d61af8', 'commit …'); // ~0.00\n```\n\nIf you're building a chat interface — over your own local model or any API — a\n**session** is the drop-in layer. One session holds one vault, so a value keeps\nthe same placeholder across every turn: mask the user's message on the way in,\nrestore the model's reply on the way out. It's model-agnostic and synchronous.\nRun `npm run benchmark`\n\non your own target runtime instead of relying on a\nhardware-independent latency claim.\n\n``` js\nimport { createSession } from 'flare-redact';\n\nconst session = createSession({ enable: ['pii'] });\n\n// on the way in — the model only ever sees placeholders\nconst prompt = session.redact(userMessage);\nconst reply = await myModel.generate(prompt);\n\n// on the way out — the user sees the real values back\nshow(session.restore(reply));\n```\n\nStreaming? Restore token by token, even when a placeholder is split across chunks:\n\n``` js\nconst out = session.stream();\nfor await (const chunk of modelStream) process(out.push(chunk.text));\nprocess(out.flush());\n```\n\n`session.redactMessages([{ role, content }])`\n\nmasks a whole chat array at once,\nincluding nested tool calls, and `session.reset()`\n\nstarts a fresh conversation.\nDetected original values stay local while your app keeps a reversible reference.\n\nAn agent loop has two directions: model-produced arguments need their local values restored before a tool executes, while tool results need new secrets masked before they enter model context. Use a separate vault for each tool or trust domain so a prompt-injected model cannot move one tool's placeholder into another tool's arguments and recover the original:\n\n``` js\nimport { createScopedToolBoundary } from 'flare-redact/tool';\n\nconst boundary = createScopedToolBoundary();\n\nconst result = await database.query('select connection_uri from services');\nconst safeResult = boundary.redactForModel('database', result);\nconst modelCall = await model.generateToolCall(safeResult);\n\n// Use a runtime-owned tool/trust-domain name, never a model-supplied scope.\nconst acceptedTool = toolRegistry.resolve(modelCall.name);\nconst localCall = boundary.restoreForTool(acceptedTool.scope, modelCall);\nawait executeTool(localCall);\n```\n\nOnly placeholders minted inside `acceptedTool.scope`\n\nare restored. Unknown and\ncross-scope placeholders stay opaque. `restoreForApp()`\n\ncan restore every scope\nonly at the final trusted application boundary, and `reset(scope?)`\n\nclears one\nscope or the whole conversation. Scope count is bounded by default.\n\nLegacy warning:`createToolBoundary()`\n\nremains backward compatible for a single tool or single trust domain, but`restoreForTool()`\n\nrestoresanyplaceholder known to that boundary. Do not share one legacy boundary across mutually untrusted tools. Prefer`createScopedToolBoundary()`\n\nfor new agent and MCP integrations.\n\nFor safe logging without reversibility, use `redactToolCall()`\n\n,\n`redactToolResult()`\n\n, or `redactMcpMessage()`\n\nfrom the same entry point.\n\nDetectors can't know your product codenames, project names, or internal jargon —\nso hand them a list. `terms`\n\ncatches exactly the words you name (any language,\nlongest match first, word-boundary safe), one-way or reversibly.\n\n```\n// one-way, with your own replacement text\nredact('Launch Project Zeus with Falcon', {\n  terms: { 'Project Zeus': '[CLASSIFIED]', 'Falcon': '[CLASSIFIED]' },\n});\n// → 'Launch [CLASSIFIED] with [CLASSIFIED]'\n\n// reversible — send to a model, get it back\nconst vault = createVault({ terms: ['Project Zeus'] });\nconst safe = vault.redact('ship Project Zeus');   // 'ship [FR_CUSTOM_TERM_a17c…]'\nvault.restore(safe);                               // 'ship Project Zeus'\n```\n\nThe same works from the CLI, including a full round-trip — mask, send the safe text anywhere, then restore what comes back:\n\n```\n# add words inline or from a file, and write an encrypted vault\nexport FLARE_REDACT_VAULT_PASSWORD='read-this-from-your-secret-manager'\nflare-redact --term \"Project Zeus\" --terms codenames.txt --vault map.json < in > safe\n\n# later, restore the originals from that map\nflare-redact --restore map.json < safe > original\n```\n\n`scan()`\n\nfinds secrets without changing the input, explains every hit in plain\nEnglish, and reports one-based line/column locations — without returning the raw\nsecret by default.\n\n``` js\nimport { scan } from 'flare-redact';\n\nscan('deploy with password=hunter2 and AKIAIOSFODNN7EXAMPLE');\n// →\n// [\n//   { detector: 'generic_assignment', label: 'Assigned secret',\n//     why: 'A value assigned to a sensitive-looking field name…', start: 12, … },\n//   { detector: 'aws_access_key', label: 'AWS access key ID',\n//     why: 'Pairs with a secret key to control cloud resources and billing.', start: 33, … },\n// ]\n```\n\nTrusted diagnostics can request the original span with\n`scan(input, { includeValues: true })`\n\n. Never enable that option for logs, CI\nreports, analytics, or error tracking.\n\nNeed just the shape of it?\n\n``` js\nimport { isClean, summary } from 'flare-redact';\n\nisClean(payload);   // → false\nsummary(payload);   // → { total: 3, byDetector: { email: 1, github_token: 1, sensitive_key: 1 } }\n```\n\n`wrapConsole`\n\npatches `console.*`\n\nso every argument is redacted on the way out,\nand hands you a function to undo it.\n\n``` js\nimport { wrapConsole } from 'flare-redact';\n\nconst restore = wrapConsole();\nconsole.log('session', { user: 'bob@x.io', token: 'ghp_…' });\n// session { user: 'b***@***', token: 'ghp_***' }\nrestore();\n```\n\nPrefer to be explicit? Bind your options once and reuse it:\n\n``` js\nimport { createRedactor } from 'flare-redact';\n\nconst safe = createRedactor({ enable: ['high_entropy'] });\nlogger.info(safe.redact({ event: 'checkout', user }));\n```\n\nDefine what \"sensitive\" means once, and apply it at every layer — your app, your logger, your HTTP boundary, your LLM calls. Every adapter takes the same options object, so a secret is masked the same way across the whole system.\n\n``` js\nimport { definePolicy } from 'flare-redact';\nconst policy = { enable: ['high_entropy'], allow: ['status@acme.com'] };\n```\n\n**pino** — reads the values, not a list of field paths you have to maintain:\n\n``` python\nimport pino from 'pino';\nimport { pinoRedact } from 'flare-redact/pino';\n\nconst log = pino(pinoRedact(policy));\nlog.info({ user: 'bob@corp.com' }); // → {\"user\":\"b***@***\"}\n```\n\n**winston** — a format that redacts every field, symbol metadata left intact:\n\n``` python\nimport winston from 'winston';\nimport { winstonRedact } from 'flare-redact/winston';\n\nwinston.format.combine(winston.format(winstonRedact(policy))(), winston.format.json());\n```\n\n**HTTP** — a safe-to-log snapshot of a request; the live request is untouched.\nThe URL string, query object, params, headers, and body are all sanitized:\n\n``` js\nimport { httpRedactor } from 'flare-redact/http';\n\napp.use(httpRedactor(policy));\napp.use((req, _res, next) => { logger.info(req.redacted()); next(); });\n// Authorization and Cookie headers, and any secret in the body or query, are masked.\n```\n\nSame `policy`\n\nobject flows into `flare-redact/llm`\n\n, `wrapConsole`\n\n, `createVault`\n\n,\nand `redactStream`\n\ntoo.\n\nPipe any log stream through it. Secrets may be split across chunks, and bounded multiline PEM private keys are masked as one record. Unterminated private keys fail closed instead of leaking their remaining bytes.\n\n``` js\nimport { redactStream } from 'flare-redact/stream';\n\nprocess.stdin.pipe(redactStream()).pipe(process.stdout);\n```\n\nPoint it at a JSON or CSV dump with `--mode surrogate`\n\nand you get deterministic,\ntyped synthetic values. The same input maps the same way in every row under one\nkey, so joins survive without calling the transformation encryption or anonymity.\n\n```\nexport FLARE_REDACT_SECRET='read-this-from-your-secret-manager'\nflare-redact --csv --mode surrogate < customers.csv > customers.safe.csv\nAlice,alice@corp.com,4242 4242 4242 4242      Alice,user_93a78c61e204@example.invalid,7042 5270 7797 8927\nBob,bob@corp.com,5555 5555 5555 4444     →    Bob,user_441ae72c0901@example.invalid,0888 2706 6232 0274\nAlice,alice@corp.com,4242 4242 4242 4242      Alice,user_93a78c61e204@example.invalid,7042 5270 7797 8927\n```\n\n`redactCsv(text, opts)`\n\nis available from `flare-redact/csv`\n\nfor the same thing\nin code.\n\nStop PII from reaching an analytics, telemetry, or webhook endpoint — wrap\n`fetch`\n\nand name the hosts you don't trust with the real data. Every other\nrequest goes through untouched, so your real API calls are never altered.\n\n``` js\nimport { wrapFetch } from 'flare-redact/fetch';\n\nconst fetch = wrapFetch(globalThis.fetch, { hosts: ['api.segment.io', 'telemetry.vendor.com'] });\n// bodies sent to those hosts are redacted; everything else is left alone\n```\n\n`scan`\n\nfrom code, or `--scan`\n\nfrom the CLI (which exits non-zero on a hit) — drop\nit into CI or a pre-commit hook. File scans report `file:line:column`\n\n, while\nmachine-readable JSON and SARIF reports never echo the matched secret value:\n\n```\n- uses: actions/checkout@v5\n- uses: actions/setup-node@v5\n  with:\n    node-version: 24\n- name: Scan project text files\n  run: npx --yes --package flare-redact@1.4.1 flare-redact --scan . --exclude package-lock.json\n```\n\nThe scan runs on the GitHub runner, reports safe file and source locations, and\nfails without sending repository contents to an external scanning service. A\ncopy-ready workflow lives in [ examples/github-secret-scan](/flare-collection/flare-redact/blob/main/examples/github-secret-scan).\nRecursive scans do not follow symlinks and skip binary files, files over 1 MiB,\nand\n\n`.git`\n\n, `.hg`\n\n, `.svn`\n\n, `node_modules`\n\n, and `vendor`\n\ndirectories by default.\nUse repeatable `--exclude`\n\nglobs, `--max-file-size`\n\n, or\n`--no-default-excludes`\n\nto change that policy.\nExclude globs support `*`\n\n, `?`\n\n, and `**`\n\n; a pattern without `/`\n\nmatches that\nbasename at any depth.\n\n```\nflare-redact --scan --format json .env app.log > flare-redact.json\nflare-redact --sarif .env app.log > flare-redact.sarif\nnpm install -g flare-redact\ntail -f app.log | flare-redact               # stream redacted logs\nFLARE_REDACT_SECRET=… flare-redact --json --mode hash < event.json\nFLARE_REDACT_SECRET=… flare-redact --csv --mode surrogate < dump.csv\nflare-redact --scan config.env               # list findings + why (exit 1 if any)\nflare-redact --scan .                        # recursively scan a project\nflare-redact --scan . --exclude 'test/**' --max-file-size 2mb\nflare-redact --scan --format json .env app.log # safe machine-readable report\nflare-redact --sarif .env > results.sarif    # GitHub code-scanning report\nflare-redact --summary --json < event.json   # counts per detector\nflare-redact --enable high_entropy < app.log # also catch unknown-format keys\nflare-redact --scan --min-confidence 0.9 .env  # only high-confidence findings\nflare-redact --enable high_entropy --refine-confidence --min-confidence 0.5 < app.log # drop UUID/SHA noise\nflare-redact --list                          # show every detector\n```\n\nOn by default:\n\n| Detector | Finds |\n|---|---|\n`private_key` |\nPEM private key blocks (RSA/EC/OpenSSH/PGP) |\n`aws_access_key` |\nAWS access key IDs (`AKIA…` , `ASIA…` ) |\n`aws_secret_key` |\nAWS secret access keys in assignments (`aws_secret_access_key=…` , `\"secretAccessKey\": …` ) |\n`github_token` |\nGitHub PATs and OAuth tokens (`ghp_…` , `github_pat_…` ) |\n`gitlab_token` |\nGitLab PATs (`glpat-…` ) |\n`slack_token` |\nSlack tokens (`xoxb-…` ) |\n`stripe_key` |\nStripe secret / restricted keys (`sk_live_…` , `rk_…` ) |\n`anthropic_key` |\nAnthropic API keys (`sk-ant-…` ) |\n`openai_key` |\nOpenAI API keys (`sk-…` ) |\n`google_api_key` |\nGoogle API keys (`AIza…` ) |\n`sendgrid_key` |\nSendGrid API keys (`SG.…` ) |\n`twilio_key` |\nTwilio SIDs / keys (`AC…` , `SK…` ) |\n`npm_token` |\nnpm tokens (`npm_…` ) |\n`jwt` |\nJSON Web Tokens |\n`bearer_token` |\n`Authorization: Bearer …` |\n`basic_auth` |\n`Authorization: Basic …` |\n`url_credentials` |\npasswords inside connection strings |\n`generic_assignment` |\n`password=` , `api_key: …` , `secret=…` (any language) |\n`email` |\nemail addresses |\n`obfuscated_email` |\nbracket-obfuscated emails such as `name [at] host [dot] tld` |\n`credit_card` |\ncard numbers (Luhn-validated) |\n`iban` |\nIBANs (mod-97 validated) |\n`openrouter_key` / `huggingface_token` / `groq_key` / `xai_key` / `perplexity_key` / `replicate_token` |\nmore AI provider keys |\n`discord_bot_token` / `discord_webhook` / `telegram_bot_token` |\nchat tokens and webhook URLs |\n`shopify_token` / `square_token` / `stripe_webhook_secret` |\ncommerce secrets |\n`digitalocean_token` / `azure_storage_key` / `vault_token` / `databricks_token` |\ncloud & infra secrets |\n`sentry_dsn` / `new_relic_key` |\nobservability secrets |\n`airtable_pat` / `postman_key` / `linear_key` / `figma_token` / `notion_token` |\nSaaS workspace tokens |\n`doppler_token` / `supabase_key` / `netlify_token` / `mailgun_key` |\nplatform API keys |\n\nOpt in with `enable`\n\n:\n\n| Detector / tag | Finds |\n|---|---|\n`high_entropy` |\nlong random-looking tokens of any format (entropy-based) |\n`crypto` |\nBitcoin & Ethereum addresses, BIP39 seed phrases |\n`finance` |\nSWIFT/BIC, US ABA routing numbers |\n`vehicle` |\nVINs (checksum-validated) |\n`network` |\nIPs, MAC addresses, coordinates, internal URLs |\n`phone` |\nE.164 and formatted national numbers (`+90 532 123 45 67` , `(555) 123-4567` , `0532 123 45 67` ) — digit-count validated, date-safe |\n\nPlus object values whose **key name** is sensitive (`password`\n\n, `token`\n\n,\n`authorization`\n\n, `cookie`\n\n, `cvv`\n\n, …) are masked regardless of content.\n\nSecrets like API keys and card numbers don't care what language your app is in.\nNeither does this — but the word-based checks do, so words like *password*,\n*secret*, and *token* are recognized as assignments and as object keys in all\n**24 languages** below:\n\n🇬🇧 English `password` |\n🇨🇳 Chinese `密码` |\n🇮🇳 Hindi `पासवर्ड` |\n🇪🇸 Spanish `contraseña` |\n🇸🇦 Arabic `كلمة المرور` |\n🇫🇷 French `mot de passe` |\n🇵🇹 Portuguese `senha` |\n🇷🇺 Russian `пароль` |\n🇯🇵 Japanese `パスワード` |\n🇩🇪 German `passwort` |\n🇰🇷 Korean `비밀번호` |\n🇹🇷 Turkish `şifre` |\n🇮🇹 Italian `segreto` |\n🇮🇷 Persian `رمز عبور` |\n🇵🇱 Polish `hasło` |\n🇺🇦 Ukrainian `пароль` |\n🇳🇱 Dutch `wachtwoord` |\n🇻🇳 Vietnamese `mật khẩu` |\n🇮🇩 Indonesian `kata sandi` |\n🇹🇭 Thai `รหัสผ่าน` |\n🇬🇷 Greek `κωδικός` |\n🇮🇱 Hebrew `סיסמה` |\n🇦🇿 Azerbaijani `şifrə` |\n🇷🇴 Romanian `parolă` |\n\nNational IDs are opt-in and **checksum-validated**, so a random run of digits is\nnever mistaken for one. Enable a whole group or a single country by tag:\n\n```\nredact(text, { enable: ['pii'] });        // every national ID below\nredact(text, { enable: ['tr', 'de'] });   // just Turkish and German\n```\n\n| Detector | Country | Validated by |\n|---|---|---|\n`iban` |\n🌐 international (on by default) |\nISO 13616 mod-97 |\n`tr_tckn` |\n🇹🇷 Turkey | TCKN checksum |\n`de_tax_id` |\n🇩🇪 Germany | ISO 7064 mod-11,10 |\n`es_dni` |\n🇪🇸 Spain (DNI/NIE) | control letter mod-23 |\n`it_codice_fiscale` |\n🇮🇹 Italy | odd/even table |\n`br_cpf` |\n🇧🇷 Brazil | two mod-11 digits |\n`nl_bsn` |\n🇳🇱 Netherlands | 11-test |\n`pl_pesel` |\n🇵🇱 Poland | weighted mod-10 |\n`ca_sin` |\n🇨🇦 Canada | Luhn |\n`us_ssn` |\n🇺🇸 United States | issued-range rules |\n`uk_nhs` |\n🇬🇧 United Kingdom (NHS) | weighted mod-11 |\n`fr_nir` |\n🇫🇷 France (NIR) | INSEE mod-97 key |\n`in_aadhaar` |\n🇮🇳 India (Aadhaar) | Verhoeff |\n`au_tfn` |\n🇦🇺 Australia (TFN) | weighted mod-11 |\n`cn_resident_id` |\n🇨🇳 China | ISO 7064 mod-11,2 |\n`jp_my_number` |\n🇯🇵 Japan (My Number) | weighted mod-11 |\n\nEvery algorithm has its own tests against known-valid and known-invalid numbers,\nso enabling them won't turn your logs into a wall of `[REDACTED]`\n\n.\n\nTeach it your own secrets, and tell it what to leave alone:\n\n```\nredact(text, {\n  custom: [{\n    id: 'internal_ticket',\n    label: 'Internal ticket',\n    why: 'Leaks internal issue-tracker IDs.',\n    pattern: /\\bACME-\\d{4,6}\\b/g,\n    mask: () => '[TICKET]',\n    default: true,\n  }],\n  allow: ['support@acme.com'],        // never redact these exact values\n  redactKeys: ['ssn', 'dob'],         // extra sensitive object keys\n});\nredact<T>(input: T, opts?): T                 // masked copy, same shape\nredactAsync<T>(input: T, opts?): Promise<T>   // supports async local NER providers\nscan(input, opts?): Finding[]                 // findings + why, input untouched\nscanAsync(input, opts?): Promise<Finding[]>   // supports async local NER providers\nisClean(input, opts?): boolean                // any secrets at all?\nisCleanAsync(input, opts?): Promise<boolean>\nsummary(input, opts?): { total, byDetector, byRisk }\ncompilePolicy(opts)                            // pre-resolved reusable sync + async policy\ncreateRedactor(opts) / definePolicy(opts)      // compatibility names for compilePolicy\nwrapConsole(opts?, console?): () => void      // patch console.*, returns restore\n\ncreateVault(opts?): Vault                      // reversible: redact / restore / entries\nrestore(input, vaultOrMap): T                  // put originals back\nsealVault(vaultOrEntries, password): Promise<SealedVaultV1>\nopenVault(envelope, password): Promise<Array<[placeholder, original]>>\n\n// adapters — each takes the same options object\npinoRedact(opts?)        // 'flare-redact/pino'    → { formatters: { log } }\nwinstonRedact(opts?)     // 'flare-redact/winston' → a format transform\nredactHttp(req, opts?)   // 'flare-redact/http'    → safe-to-log request snapshot\nredactUrl(url, opts?)    // 'flare-redact/http'    → sanitized absolute/relative URL\nhttpRedactor(opts?)      // 'flare-redact/http'    → Express/Connect middleware\nredactCsv(text, opts?)   // 'flare-redact/csv'     → anonymize a CSV dataset\nwrapFetch(fetch, opts?)  // 'flare-redact/fetch'   → redact egress to named hosts\ncreateRedactionMiddleware(opts?) // 'flare-redact/middleware' → any function/handler\n\n// from 'flare-redact/ml'\nsecretProbability(value, context?): number      // learned secret-vs-look-alike score, 0..1\nextractFeatures(value, context?): number[]      // the raw feature vector\n\n// from 'flare-redact/llm'\nwrapOpenAI(client, opts?)                       // scrub prompts, restore replies (+streaming)\nwrapAnthropic(client, opts?)                    // same for messages.create + system\nredactPrompt(text, opts?): { text, vault }\n\n// from 'flare-redact/tool'\ncreateScopedToolBoundary(opts?)                // scope-safe model ↔ tool/MCP boundary\ncreateToolBoundary(opts?)                      // legacy single-trust-domain boundary\nredactToolCall / redactToolResult / redactMcpMessage\n\n// from 'flare-redact/stream'\nredactStream(opts?): Transform                  // chunk-safe + bounded multiline PEM redaction\n\n// opts\n// {\n//   only?, enable?, disable?, custom?,   // which detectors run\n//   mode?: 'mask' | 'label' | 'hash' | 'pseudonym' | 'surrogate',\n//   transformSecret?, mask?, minConfidence?, refineConfidence?, semanticProvider?, limits?,\n//   includeValues?: boolean,                // scan only; unsafe raw values\n//   redactKeys?: boolean | RegExp | string[],\n//   allow?: RegExp | string[],\n//   terms?: string[] | { term: replacement }, termsCaseSensitive?,\n// }\n\ncreateSession(opts?)      // chat/AI apps: redact in, restore out, streaming, reset\n```\n\nPath-based redactors (like naming fields in a logger config) only hide the fields\nyou *remembered* to name. The leak is always the field you forgot — the free-text\nmessage, the nested third-party payload, the string someone concatenated by hand.\nflare-redact scans the actual values, so it doesn't depend on your memory.\n\nBuilt-in patterns are reviewed for bounded structure, exercised by an adversarial runtime suite, and protected by per-string input and finding limits. JavaScript RegExp does not provide a formal linear-time guarantee, however, and arbitrary custom detectors are trusted code. Run the included benchmarks on your own runtime instead of treating a badge as a security proof:\n\n```\nnpm run benchmark\nnpm run benchmark:adversarial\n```\n\n- Detection is best-effort; a clean scan is not proof that data contains no PII.\n`scan()`\n\nomits raw values by default.`includeValues`\n\nintentionally puts those secrets back into process memory and must stay out of external reports.`pseudonym`\n\nis keyed, deterministic pseudonymization — not NIST FF1 encryption.- A vault map is sensitive; persist only the authenticated encrypted envelope.\n- Restoring a placeholder intentionally reveals its original locally. Do not forward restored model output to another untrusted sink automatically.\n- A model-produced tool name is untrusted input. Resolve it to a runtime-owned\ntool/trust-domain scope before calling\n`restoreForTool()`\n\n. - The legacy\n`createToolBoundary()`\n\nrestores every placeholder in its shared vault. Use`createScopedToolBoundary()`\n\nwhenever more than one tool or trust domain can receive model-produced arguments. - The 24-language badge describes secret-key vocabulary, not general multilingual\nnamed-entity recognition. Use a local\n`semanticProvider`\n\nfor that task.\n\nEncrypted vaults do not protect a compromised host or secrets already resident in process memory. Deterministic transforms reveal when two inputs are equal.\n\nMIT © Umud Hasanli", "url": "https://wpnews.pro/news/flare-redact-scoped-secret-redaction-for-javascript-ai-agents", "canonical_source": "https://github.com/flare-collection/flare-redact", "published_at": "2026-08-04 09:39:17+00:00", "updated_at": "2026-08-04 09:53:08.157014+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-safety", "ai-infrastructure"], "entities": ["Flare Redact", "flare-redact", "OpenAI", "Anthropic", "Node", "React", "Vue"], "alternates": {"html": "https://wpnews.pro/news/flare-redact-scoped-secret-redaction-for-javascript-ai-agents", "markdown": "https://wpnews.pro/news/flare-redact-scoped-secret-redaction-for-javascript-ai-agents.md", "text": "https://wpnews.pro/news/flare-redact-scoped-secret-redaction-for-javascript-ai-agents.txt", "jsonld": "https://wpnews.pro/news/flare-redact-scoped-secret-redaction-for-javascript-ai-agents.jsonld"}}