typescript 50 lines · 9 steps

Recursively masking sensitive data for logs

A key-pattern registry drives a recursive walk that redacts emails, cards, and secrets before anything hits your logs.

Explained by highlit
1type Masker = (value: string) => string;
2 
3const maskEmail: Masker = (value) => {
4 const [local, domain] = value.split("@");
5 if (!domain) return value;
6 const visible = local.slice(0, 2);
7 return `${visible}${"*".repeat(Math.max(local.length - 2, 1))}@${domain}`;
8};
9 
10const maskCard: Masker = (value) => {
11 const digits = value.replace(/\D/g, "");
12 if (digits.length < 12) return value;
13 return `**** **** **** ${digits.slice(-4)}`;
14};
15 
16const SENSITIVE_KEYS = new Map<RegExp, Masker>([
17 [/email|e_mail/i, maskEmail],
18 [/card|ccnum|creditCard/i, maskCard],
19 [/password|secret|token|apiKey/i, () => "[REDACTED]"],
20]);
21 
22function resolveMasker(key: string): Masker | undefined {
23 for (const [pattern, masker] of SENSITIVE_KEYS) {
24 if (pattern.test(key)) return masker;
25 }
26 return undefined;
27}
28 
29export function maskForLogging<T>(input: T, seen = new WeakSet<object>()): T {
30 if (Array.isArray(input)) {
31 return input.map((item) => maskForLogging(item, seen)) as unknown as T;
32 }
33 
34 if (input !== null && typeof input === "object") {
35 if (seen.has(input)) return input;
36 seen.add(input);
37 
38 const result: Record<string, unknown> = {};
39 for (const [key, value] of Object.entries(input)) {
40 const masker = resolveMasker(key);
41 result[key] =
42 masker && typeof value === "string"
43 ? masker(value)
44 : maskForLogging(value, seen);
45 }
46 return result as T;
47 }
48 
49 return input;
50}
01 / 01
STEP 01

Walkthrough

Space play ←→ step click any line
Three takeaways
  1. 1A registry of regex-to-transform pairs keeps masking rules declarative and easy to extend.
  2. 2A WeakSet of visited objects lets recursive traversal survive cyclic references without infinite loops.
  3. 3Masking by key name means new fields are protected automatically once they match an existing pattern.

Related explainers

typescript
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
import localeDe from '@angular/common/locales/de';

Locale-aware bootstrapping in Angular

i18n localization dependency-injection
Intermediate 8 steps
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
 

Validating env config at boot in NestJS

configuration schema-validation environment-variables
Intermediate 8 steps
ruby
class UserAgentParser
  BROWSERS = [
    [/Edg\/([\d.]+)/, "Edge"],
    [/OPR\/([\d.]+)/, "Opera"],

Parsing user-agent strings in Ruby

regex pattern-matching lookup-tables
Intermediate 8 steps
javascript
function evaluate(expression) {
  const tokens = tokenize(expression);
  let pos = 0;
 

Building a recursive descent calculator

parsing recursion operator-precedence
Intermediate 8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';

A cache-aside country lookup in NestJS

cache-aside dependency-injection batch-lookup
Intermediate 8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps

Share this explainer

Here's the card — post it anywhere.

Recursively masking sensitive data for logs — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code