Skip to content

martinkr/bluestreak

Repository files navigation

Bluestreak

Bluestreak Cleaner Wrasse

Stop secrets and PII from reaching your logs, search indexes, and third-party tools.

High-throughput JSON log redaction for Node.js — 600k+ lines/sec on Apple Silicon. Drop it in front of your log pipeline and every field named password, token, credit_card, or any of hundreds of locale-aware aliases becomes [**REDACTED**] before the line leaves your process. Zero runtime dependencies. TypeScript-native. Node.js 20+.


Install

npm install bluestreak

Quick start

Two lines to go from leaking logs to safe ones:

import { compileRecommendedPolicy, redactLine } from "bluestreak";

const policy = compileRecommendedPolicy();

console.log(redactLine(JSON.stringify({ password: "x", message: "[email protected]" }), policy));
// {"password":"[**REDACTED**]","message":"[**REDACTED**]"}

The recommended preset gives you English locale + GDPR/PCI keywords + email, bearer token, and auth header patterns — enough for most SaaS APIs with no configuration.

For full control:

import {
  compilePolicy,
  redactLine,
  BUILTIN_PATTERNS,
  SOURCE_CONTROL_PATTERNS,
  PAYMENT_PATTERNS,
  MESSAGING_PATTERNS,
} from "bluestreak";

const policy = compilePolicy({
  complianceProfiles: ["GDPR", "PCI_DSS"],
  locales: ["en-en"],
  scanTextFields: ["log", "message", "msg", "error", "stack", "body"],
  maxFieldScanLength: 4096,
  panSeparatorChars: " -._",
  customPatterns: [
    BUILTIN_PATTERNS.EMAIL,
    BUILTIN_PATTERNS.BEARER_TOKEN,
    BUILTIN_PATTERNS.AUTH_HEADER,
    SOURCE_CONTROL_PATTERNS.GITHUB_PAT,
    PAYMENT_PATTERNS.STRIPE_KEY,
    MESSAGING_PATTERNS.SLACK_TOKEN,
  ],
});

const line = JSON.stringify({
  level: "info",
  password: "hunter2",
  message: "charged [email protected] card 4111 1111 1111 1111",
});

console.log(redactLine(line, policy));
// {"level":"info","password":"[**REDACTED**]","message":"charged [**REDACTED**] card [**REDACTED**]"}

Compile the policy once at startup — it's a plain object you can share across workers, queue consumers, and services. Never call compilePolicy per line.


Use cases

Bluestreak fits anywhere you need consistent, policy-driven scrubbing at the log-line boundary — not just in-process object mutation.

  • Ship-side / pipeline redaction — Run redactLine on each event before logs reach Elasticsearch, Datadog, S3, or a vendor SDK so secrets and PII never leave your network, with the same policy everywhere.

  • Key-based PII and secrets — Rely on merged locale packs and compliance profiles so fields like password, token, and translated or aliased key names are redacted by normalized key matching, without hand-maintaining huge path lists.

  • Secrets inside free text — Use scanTextFields plus customPatterns / built-in patterns to redact emails, bearer tokens, card-like digit runs, and so on inside message, stack, error, or other long string fields where path rules alone are not enough.

  • Exact paths and carve-outs — Use redactPaths for values sensitive at a specific location (e.g. request.headers.authorization), and excludePaths to preserve known-safe values (e.g. a public product.id) even when the bare key would otherwise match a global sensitive list.

  • Non-JSON and broken lines — Lines that aren't valid JSON still go through raw high-risk scanning, so truncated payloads, plain-text errors, or mixed formats don't bypass redaction.

  • Shared org policy — Compile one CompiledPolicy at startup and reuse it across workers, queue consumers, and services so security and platform teams own a single definition of "redacted."

When something else is a better fit: If you only need fast path redaction on JavaScript objects inside a logger (and you already control object shape), a dedicated path engine like fast-redact (e.g. via Pino) can be lighter. Bluestreak targets line-level JSON, combined key + path + pattern behavior, and operational consistency across the full log path.


Logger integration

Recipes for wiring Bluestreak into common Node loggers (dependencies stay out of this package):


How it works

Bluestreak uses two complementary mechanisms on every line:

1. Key-name matching — at compile time, keyword lists from your chosen locales and compliance profiles are merged into a single hash set. At runtime, every JSON key is normalized (lowercased, separators stripped) and checked against that set in O(1). A hit replaces the entire value with [**REDACTED**], regardless of what the value looks like.

{ "API_KEY": "abc" }  →  normalizeKey("API_KEY") = "apikey"  →  in set  →  [**REDACTED**]
{ "api-key": "abc" }  →  normalizeKey("api-key") = "apikey"  →  in set  →  [**REDACTED**]
{ "apiKey":  "abc" }  →  normalizeKey("apiKey")  = "apikey"  →  in set  →  [**REDACTED**]

2. Pattern scanning — for fields you name in scanTextFields (typically message, log, error), the string value is scanned using the patterns you configure. Only the first maxFieldScanLength characters are scanned; the rest is appended unchanged so very long fields don't become a performance cliff.

Lines that fail JSON.parse (truncated, binary, plain text) fall through to a raw scrubber that applies the same patterns to the whole string, so nothing breaks and no sensitive content leaks on a bad line.

No patterns run by default. You choose exactly what gets scanned: use the exported BUILTIN_PATTERNS constants, patterns from a locale pack, your own regexes, or any combination.


Performance

Bluestreak is designed to stay fast when you compile once and keep hot-path work predictable. Latest benchmark on this repo's reference machine (darwin arm64, Node v20.15.0, 2026-04-20):

Scenario Throughput (lines/sec) vs baseline
Baseline (profiles + locales, no extra patterns/paths) 645 466
+10 customPatterns (medium-complexity regex set) 521 451 −19%
Exact-path: 3 redactPaths + 1 excludePaths 651 898 ≈ parity
Exact-path: 10 paths + 2 excludes 433 506 −33%
pathCensor callback (3 paths) 556 989 −14%

Benchmark: 120 000 lines, 10 000 warmup, median of 3 rounds, mixed JSON + raw lines. Run on your hardware:

npm run perf

Tuning recommendations

  1. Compile one policy per hot path — call compilePolicy at startup, never per line.
  2. Minimize customPatterns — each pattern is a full pass over every scanned text field. Prefer tight anchors (\b, known literal prefixes).
  3. Use exact paths sparingly — keep redactPaths / excludePaths to ≤ 5 entries for ultra-hot pipes. For many paths, split policies by module.
  4. Prefer a static pathCensor string over a callback in latency-sensitive code.
  5. Re-run npm run perf after material policy changes.

Stream vs non-stream mode

Mode Use when Entry point Typical APIs
Non-stream You already have a line/object in app code bluestreak redactLine, tryRedactLine, redactParsed, redactLines
Stream You are piping log output through Node streams/transports bluestreak/node createLineRedactingTransform, redactTransform

Non-stream — request handlers, jobs, CLI commands, browser logging wrappers:

import { compileRecommendedPolicy, redactLine } from "bluestreak";

const policy = compileRecommendedPolicy();
const redacted = redactLine(line, policy);

Stream — Pino/Winston transports, file pipelines, log shipping:

import { createReadStream, createWriteStream } from "node:fs";
import { compileRecommendedPolicy } from "bluestreak";
import { createLineRedactingTransform } from "bluestreak/node";

const policy = compileRecommendedPolicy();

createReadStream("app.log")
  .pipe(createLineRedactingTransform(policy, { safe: true }))
  .pipe(createWriteStream("app.redacted.log"));

Common recipes

Process a file line by line

import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
import { compilePolicy, redactLines } from "bluestreak";

const policy = compilePolicy({
  complianceProfiles: ["GDPR", "SOC2"],
  locales: ["en-en"],
  scanTextFields: ["message", "log"],
  maxFieldScanLength: 4096,
});

const rl = createInterface({
  input: createReadStream("app.log"),
  crlfDelay: Infinity,
});

for await (const line of redactLines(rl, policy)) {
  process.stdout.write(line + "\n");
}

Cover a multi-language codebase

const policy = compilePolicy({
  complianceProfiles: ["GDPR"],
  locales: ["en-en", "de-de", "fr-fr"], // password + passwort + mot_de_passe → all caught
  scanTextFields: ["message", "log"],
  maxFieldScanLength: 4096,
});

Supported locales: en-en, de-de, es-es, fr-fr.

Non-JSON input

redactLine never throws. Invalid JSON falls back to a raw scrubber that applies the same configured patterns to the whole string:

redactLine("Authorization: Bearer supersecret123", policy);
// → "Authorization: Bearer [**REDACTED**]"

redactLine("not json at all, nothing sensitive", policy);
// → "not json at all, nothing sensitive"

Processing already-parsed JSON

import { compilePolicy, redactTree } from "bluestreak";

const { value, changed } = redactTree(parsedObject, policy);
if (changed) {
  storeRedacted(JSON.stringify(value));
} else {
  storeOriginal(rawLine); // original formatting preserved, no extra allocations
}

Tenant-specific field names

import { composeKeywordSet } from "bluestreak";

const tenantFields = await db.query(
  "SELECT field_name FROM sensitive_fields WHERE tenant_id = ?",
  [tenantId],
);

const policy = composeKeywordSet({
  complianceProfiles: ["GDPR"],
  locales: ["en-en"],
  extraKeywordLists: [tenantFields.map((r) => r.field_name)],
  scanTextFields: ["message", "log"],
  maxFieldScanLength: 4096,
});

Build one policy per tenant and cache it. Don't call per log line.


Compliance profiles

Profile Adds keywords for
GDPR Names, addresses, national IDs, location, tax IDs
HIPAA Patient IDs, medical records, diagnoses, insurance
PCI_DSS Card numbers, CVV/CVC, expiry, track data
SOC2 API keys, session tokens, private keys, auth headers

Pass any combination: complianceProfiles: ["GDPR", "SOC2"]. Use "*" to merge all profile buckets at once.

Each locale pack covers its own language only. Include "en-en" explicitly whenever you need English coverage alongside other locales.


Available patterns

Key-name matching protects fields you can name. Pattern scanning protects values with a recognisable shape, regardless of the key.

Protocol-level (BUILTIN_PATTERNS) — universal, safe to add to almost any policy:

Export Matches Example
BUILTIN_PATTERNS.EMAIL Email addresses (ASCII domains) [email protected]
BUILTIN_PATTERNS.BEARER_TOKEN Bearer token value (prefix preserved) Bearer eyJhbGci…
BUILTIN_PATTERNS.AUTH_HEADER Authorization: header value Authorization: tok123
panSeparatorChars config Card numbers (Luhn-validated) 4111 1111 1111 1111

Vendor / service-specific (SERVICE_PATTERNS) — add only the services your application uses:

Category Patterns
SOURCE_CONTROL_PATTERNS GitHub (4 token types), GitLab (3 types), npm
CLOUD_PATTERNS AWS, Google Cloud / Firebase / Gemini, DigitalOcean, HashiCorp Vault
PAYMENT_PATTERNS Stripe (secret + restricted), Square (OAuth + app secret)
MESSAGING_PATTERNS Slack, Twilio, SendGrid, Mailgun
ECOMMERCE_PATTERNS Shopify, WooCommerce
MONITORING_PATTERNS New Relic, Grafana, PlanetScale
CRM_PATTERNS HubSpot, Mailchimp
AI_PATTERNS OpenAI (legacy + project-scoped), Anthropic, Hugging Face
AUTH_PATTERNS JSON Web Token

All patterns are also available on SERVICE_PATTERNS as a flat object, and individually by name (e.g. GITHUB_PAT_PATTERN, AWS_ACCESS_KEY_PATTERN).


API reference

compilePolicy(config)

Builds a policy from locale packs and compliance profiles. Returns a CompiledPolicy safe to share across calls and workers. Call once at startup.

const policy = compilePolicy({
  complianceProfiles: ["*"],
  locales: ["en-en"],
  preset: "logging",                // "logging" | "strict" | "recommended"
  scanTextFields: ["message", "log"],
  maxFieldScanLength: 4096,
  extraKeywordLists: [["tenant_secret_key"]],
  panSeparatorChars: " -._",
  customPatterns: [],
  redactPaths: ["session.id", "request.headers.authorization"],
  excludePaths: ["product.id"],
  pathCensor: "[**REDACTED**]",     // string or (value, path) => unknown
});

compileRecommendedPolicy(config?)

Shorthand for compilePolicy({ ...config, preset: "recommended" }). Defaults to en-en locale, GDPR + PCI_DSS profiles, and baseline text patterns.

redactLine(raw, policy)

Main entry point. Parses raw as JSON, redacts the tree, returns a string. Unchanged lines are returned as-is (no extra allocations). Falls back to raw scrubber on invalid JSON.

tryRedactLine(raw, policy)

Never throws. Returns raw unchanged if redaction fails for any reason. Use on hot sinks.

redactLines(lines, policy)

Async generator wrapper around redactLine. Works with any AsyncIterable<string>.

redactParsed(value, policy)

Redacts an already-parsed object — skips JSON.parse when you already have a value.

redactTree(node, policy)

Redacts an already-parsed JSON value using copy-on-write. Returns { value, changed }.

composeKeywordSet(input)

Same option surface as compilePolicy. Use for runtime keyword composition (tenant-specific lists, per-request overrides).

normalizeKey(raw)

The same key normalization used internally: lowercase ASCII, keep digits, strip everything else.

normalizeKey("X-Api-Key"); // → "xapikey"
normalizeKey("user_id");   // → "userid"

Stream API — import from bluestreak/node

Function Description
createLineRedactingTransform(policy, options?) Buffers UTF-8 chunks, splits on \n, redacts each complete line. options.safe uses tryRedactLine.
redactTransform(policy) Node.js Transform where each chunk is a single complete log line. Split first if chunks don't align to newlines.

Advanced

Custom locale packs

Define a LocaleKeywordPack in your own codebase — no fork required:

import type { LocaleKeywordPack } from "bluestreak";

export const myPack: LocaleKeywordPack = {
  generalKeywords: ["vault_token", "internal_secret"],
  profileKeywords: {
    GDPR: ["customer_ref", "account_holder"],
    HIPAA: ["mrn_internal"],
    PCI_DSS: ["payment_ref"],
    SOC2: [],
  },
  patterns: [
    /\bINT-\d{8}\b/g,          // internal transaction ID
    /\bACCT-[A-Z0-9]{12}\b/g,  // account reference
  ],
};

const policy = compilePolicy({
  locales: ["en-en", myPack],
  complianceProfiles: ["GDPR"],
  scanTextFields: ["message", "log"],
  maxFieldScanLength: 4096,
});

Locale pack patterns vs customPatterns

LocaleKeywordPack.patterns customPatterns in compilePolicy
Use for Jurisdiction-specific PII formats — IBANs, national IDs, local phone formats App-specific formats — internal token shapes, external services
Scope Bound to a locale pack Global
Examples German IBAN, French NIR AWS access keys, internal order IDs

At runtime, both are merged into the same flat list and applied identically.

Card number detection

// Enable with common separators
const policy = compilePolicy({ panSeparatorChars: " -._" });

// Also match pipe-separated formats
const policy = compilePolicy({ panSeparatorChars: " -._|" });

// Contiguous digits only (no separators)
const policy = compilePolicy({ panSeparatorChars: "" });

Key-name matching (card_number, pan, etc.) catches the value regardless of format.


Tests and performance

npm test              # run the test suite (vitest)
npm run test:coverage # coverage report
npm run perf          # throughput benchmark — results appended to PERFORMANCE_RESULTS.md

License

MIT

About

Stop secrets and PII from reaching your logs, search indexes, and third-party tools.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors