npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@asaidimu/utils-sanitize

v1.0.12

Published

A collection of sanitize utilities.

Readme

@asaidimu/utils-sanitize

Field-level sanitization for documents, with support for configurable masking policies, recursive deep sanitization, multi-scope configuration merging, and HMAC-based hashing. Works in browsers and Node.js 18+ (Web Crypto API).

Installation

npm install @asaidimu/utils-sanitize

Quick Start

import {
  DocumentSanitizer,
  newSecureDefaultConfig,
  commonEnvPatterns,
} from "@asaidimu/utils-sanitize";

// Create a sanitizer with common security patterns + env var value masking
const config = newSecureDefaultConfig();
config.patterns = [...config.patterns!, ...commonEnvPatterns(["DATABASE_URL"])];
const sanitizer = new DocumentSanitizer(config, "my-scope");

const doc = {
  username: "alice",
  password: "supersecret",
  email: "[email protected]",
  ssn: "123-45-6789",
  k: process.env.API_KEY, // caught by value-pattern even though key is "k"
  notes: "hello world",
};

const result = await sanitizer.sanitizeDocumentDeep(doc);
// {
//   username: "alice",
//   password: "***",
//   email:    "al*********om",
//   ssn:      "***",
//   k:        "***",
//   notes:    "hello world",
// }

Policies

| Policy | Constant | Effect | | ------------ | -------------- | ------------------------------------------------------------------------------- | | "redact" | MaskRedact | Replaces value with "***" | | "hash" | MaskHash | Replaces value with [HASH:XXXXXXXX] (first 8 hex chars of HMAC-SHA256) | | "obscure" | MaskObscure | Shows prefix/suffix characters, replaces middle with a character (default: *) | | "preserve" | MaskPreserve | Leaves the value unchanged (default) |

Policy Resolution

Every field goes through resolvePolicy(fieldName, value), which walks all matching rules and returns the most restrictive result — it never short-circuits on the first match.

Resolution collects candidates from three sources:

  1. Explicit field mapping (config.fields) — keyed by exact field name
  2. Key-patterns (matchOn: "key") — tested against the field name
  3. Value-patterns (matchOn: "value") — tested against the string representation of the field value, regardless of key name

The most restrictive policy across all matches wins. For example, if a key-pattern resolves to "obscure" but a value-pattern resolves to "redact", the field is redacted.

Restrictiveness Ordering

redact > hash > obscure > preserve

Use mostRestrictivePolicy(a, b) to get the more restrictive of two policies.

Input Normalization

Before any policy is applied, all document entry points normalize the input through toPojo. This ensures the sanitizer output is always a serialization-safe plain object, since the output is assumed to be serialized (e.g. via JSON.stringify).

Normalization rules (applied recursively):

  • Own string keys only — inherited properties and symbol-keyed properties are discarded
  • Functions are dropped — function-valued keys are removed entirely; function elements inside arrays are compacted out
  • Symbols are dropped — same treatment as functions
  • null / undefined — preserved as-is
  • Date / Uint8Array — treated as leaf scalars, returned unchanged
  • Arrays — recursed element-by-element; non-serializable elements (functions, symbols) are removed, compacting the array
  • All other objects — own string keys only, recursed into, producing a plain Record
  • Primitives (string, number, boolean, bigint) — pass through unchanged

API

Core Types

type MaskedFieldPolicy = "redact" | "hash" | "preserve" | "obscure";

Sanitizer

Flat-document sanitizer. Applies policies to top-level fields only.

const s = new Sanitizer(config, logger?);

// Sanitize a single value
await s.sanitizeValue("fieldName", value);

// Sanitize a flat document (input is normalized via toPojo)
await s.sanitizeDocument({ field: value, ... });

DocumentSanitizer

Extends Sanitizer with recursive deep sanitization and metadata handling.

const ds = new DocumentSanitizer(config, scopeID, logger?);

// Deep sanitization (nested objects, arrays; input normalized via toPojo)
await ds.sanitizeDocumentDeep(doc);

// Sanitize only metadata (preserves reserved system fields)
await ds.sanitizeMetadata(metadata);

Deep sanitization behaviour:

  • Plain objects — recursed, each key sanitized independently (policies are not inherited from parent field names)
  • Arrays — recursed element-by-element
    • Objects inside arrays have each key sanitized independently
    • Scalars inside arrays inherit the parent field's policy via <fieldName>[] lookup
  • Dates and Uint8Arrays — treated as scalars (not recursed)
  • _metadata_ — special handling: system fields (_version_, _created_, _updated_, _checksum_, _signature_) are preserved; user-defined fields are sanitized

FieldMaskConfig

interface FieldMaskConfig {
  version?: string;
  scope?: string;
  defaultPolicy?: MaskedFieldPolicy; // default: "preserve"
  fields?: Record<string, MaskedFieldPolicy>;
  patterns?: PatternRule[];
  obscureConfig?: ObscureConfig;
  hashSecret?: string; // hex-encoded, ≥ 32 hex chars
  description?: string;
}

ObscureConfig

interface ObscureConfig {
  prefixLength: number; // chars to show at start (default: 2)
  suffixLength: number; // chars to show at end (default: 2)
  replacement: string; // middle replacement char (default: "*")
  maxLength: number; // normalize output length (0 = no limit, default: 0)
}

When maxLength is set, the output is normalised to exactly maxLength, hiding the original value's length:

| Original | maxLength=12 | Notes | | ------------------- | ---------------- | --------------- | | "abc123" | "ab******23" | Padded to 12 | | "1ea82440-9c3e" | "1ea8****9c3e" | Exact fit | | "1ea82440-…-2651" | "1ea8****2651" | Truncated to 12 |

Values shorter than prefixLength + suffixLength + 1 are rendered as "[OBSCURED]".

PatternRule

interface PatternRule {
  pattern: string; // regex source string
  policy: MaskedFieldPolicy;
  matchOn?: "key" | "value"; // default: "key"
  comment?: string; // human-readable description
  readonly _regex?: RegExp; // compiled regex (non-enumerable, internal)
}

matchOn controls what the pattern is tested against:

  • "key" — pattern is tested against the field name. Used by commonSecurityPatterns().
  • "value" — pattern is tested against the string representation of the field value, regardless of key name. Used by commonEnvPatterns() to catch cases like { k: process.env.API_KEY } where the key carries no signal.

Both kinds of rules participate in the same resolvePolicy pass, with the most restrictive match across all rules winning.

Config Validation

validateConfig(config) validates a FieldMaskConfig, mutating it to apply defaults. Throws SystemError aggregating all validation errors when any are found (severity: "error"). Warnings (e.g., maxLength too small) are collected but do not throw. Also validates matchOn when present.

Configuration Merging

mergeConfigs(globalConfig, scopedConfig): FieldMaskConfig

Merges a global config with a scoped config for multi-scope setups:

  • Fields — global baseline, scoped overrides (scoped wins on collision)
  • Patterns — scoped patterns evaluated first (higher priority); a scoped pattern shadows a global pattern only when both have the same matchOn and the same regex source. A "key" pattern and a "value" pattern with the same regex are treated as distinct rules and never collapse.
  • Default policy — scoped wins, then global, then "preserve"
  • Obscure config — scoped wins when it has a replacement char
  • Hash secret — scoped wins

Does not mutate either input.

Helpers

// Field policy parsing
parseMaskedFieldPolicy(s: string): MaskedFieldPolicy;  // throws SystemError on invalid

// Policy comparison
mostRestrictivePolicy(a, b): MaskedFieldPolicy;

// Input normalization
toPojo(value: unknown): unknown;  // recursively normalize to serialization-safe POJO

// Pattern compilation
compilePattern(pattern, policy, comment?, matchOn?): PatternRule;     // throws on invalid regex
mustCompilePattern(pattern, policy, comment?, matchOn?): PatternRule; // alias (for static init)

// Pre-built key-name patterns
commonSecurityPatterns(): PatternRule[];   // 11 patterns matched against field names
newSecureDefaultConfig(): FieldMaskConfig; // config with common patterns + defaults

// Pre-built value patterns from environment variables
commonEnvPatterns(extraKeys?: string[], env?: Record<string, string | undefined>): PatternRule[];

// Batch sanitization
sanitizeDocumentArray(sanitizer, docs[]): Promise<Record<string, unknown>[]>;
sanitizeValue(sanitizer, value): Promise<unknown>; // auto-detect object/array/scalar

commonSecurityPatterns() — included patterns

All rules use matchOn: "key" — they match against field names.

| Pattern | Policy | Field examples | | -------------------------- | --------- | ------------------------------ | | password | redact | password, userPassword | | secret | redact | secret, my_secret | | token | redact | token, auth_token | | api[_-]?key | redact | api_key, apikey, api-key | | private[_-]?key | redact | private_key | | credential | redact | credential, credentials | | ssn\|social[_-]?security | redact | ssn, social_security | | credit[_-]?card\|cvv | redact | credit_card, cvv | | email | obscure | email, userEmail | | phone\|mobile | obscure | phone, mobile | | auth | hash | auth, authToken |

commonEnvPatterns(extraKeys?, env?) — value patterns from env

Builds matchOn: "value" rules from environment variables so that any field whose value equals a known secret is redacted, regardless of key name. This catches patterns like { k: process.env.API_KEY } where the key carries no signal.

const config = newSecureDefaultConfig();
config.patterns = [
  ...config.patterns!,
  // heuristic: all env vars whose name contains key, secret, token, etc.
  // + any explicitly named vars
  ...commonEnvPatterns(["DATABASE_URL"]),
];

How rules are built:

  1. Collects env var names whose name matches the heuristic (key, secret, token, password, passwd, credential, auth, api, private, cert, signing — case-insensitive)
  2. Merges in extraKeys; deduplicates by name
  3. Skips values shorter than 8 characters (too short — high false-positive risk)
  4. Deduplicates by resolved value — two vars holding the same secret produce one rule
  5. Escapes each value as a literal regex so special characters do not alter matching semantics
  6. Returns PatternRule[] with matchOn: "value" and policy: "redact"

The optional env parameter (default: process.env) accepts any Record<string, string | undefined>, making the function fully testable without real environment state:

const rules = commonEnvPatterns(["MY_KEY"], { MY_KEY: "super-secret-value" });
// → one PatternRule, matchOn "value", literal pattern "super\-secret\-value"

Hash Details

  • Uses HMAC-SHA256 via the Web Crypto API (crypto.subtle.sign)
  • Output format: [HASH:XXXXXXXX] (first 8 hex chars)
  • Each Sanitizer instance uses a per-instance random 32-byte secret when hashSecret is not provided, preventing rainbow-table attacks
  • Consistent secret across instances = consistent hashes for the same input values

Obscure Details

  • Uses TextDecoder for Uint8Array values
  • Falls back to String() for all other non-string types
  • With maxLength > 0, the output is normalised to exactly maxLength characters, hiding the original length

Error Handling

The module throws SystemError (from @core/error) with structured error codes:

| Code | Scenario | | ---------------------------------- | ----------------------------------------------- | | ERR_SANITIZATION_CONFIG_INVALID | Config validation failed | | ERR_INVALID_POLICY | Unknown policy string | | ERR_INVALID_MATCH_ON | matchOn value is not "key" or "value" | | ERR_EMPTY_PATTERN | Pattern string is empty | | ERR_INVALID_REGEX | Pattern regex failed to compile | | ERR_INVALID_CONFIG | Invalid obscure config or hash secret | | ERR_SANITIZATION_PATTERN_INVALID | compilePattern received invalid regex | | ERR_SANITIZATION_INVALID_INPUT | Top-level document is not a plain object | | ERR_SANITIZATION_FAILED | Document sanitization failed (batch operations) |

On invalid config, Sanitizer and DocumentSanitizer constructors fall back to a preserve-all default and log the error — they never throw from the constructor.