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

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