← Runnable Projects

阅读中文版本

Recursive JSON Secret Masker

A library and CLI that deep-masks sensitive keys in any JSON structure, handles arrays and nesting, and never touches safe keys, with tests for edge shapes.

beginnerjavascriptunverified

Project

node-json-mask

Entry point: mask.mjs

Install

node --version

Run

node mask.mjs payload.json

Test

node --test mask.test.mjs

Expected output

Given a payload with password and apiToken fields, the CLI prints the same JSON with those values replaced by the string ***.

Dependencies

  • node >=20

Security note

Key-name matching is allowlist-driven; add domain-specific sensitive keys to SENSITIVE_KEYS rather than masking by value patterns.

What it does

Deep-masks sensitive keys in any JSON structure, replacing values with *** while leaving safe fields untouched. The original object is never mutated.

import { readFileSync } from 'node:fs';

const SENSITIVE_KEYS = new Set(['password', 'passwd', 'secret', 'token', 'apiToken', 'apiKey', 'authorization', 'cookie']);
const MASK = '***';

export function maskValue(value) {
  if (Array.isArray(value)) return value.map(maskValue);
  if (value !== null && typeof value === 'object') {
    const out = {};
    for (const [key, v] of Object.entries(value)) {
      out[key] = SENSITIVE_KEYS.has(key) && typeof v !== 'object' ? MASK : maskValue(v);
    }
    return out;
  }
  return value;
}

const [, , file] = process.argv;
if (file) {
  const doc = JSON.parse(readFileSync(file, 'utf8'));
  console.log(JSON.stringify(maskValue(doc), null, 2));
}
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { maskValue } from './mask.mjs';

test('masks sensitive keys at any depth and inside arrays', () => {
  const input = {
    user: 'amy',
    password: 'hunter2',
    nested: { apiKey: 'abc', safe: 'keep' },
    sessions: [{ token: 't1' }, { token: 't2', ok: 1 }],
  };
  const out = maskValue(input);
  assert.equal(out.password, '***');
  assert.equal(out.nested.apiKey, '***');
  assert.equal(out.nested.safe, 'keep');
  assert.equal(out.sessions[0].token, '***');
  assert.equal(out.sessions[1].ok, 1);
  assert.equal(out.user, 'amy');
});

test('null and primitive inputs pass through unchanged', () => {
  assert.equal(maskValue(null), null);
  assert.equal(maskValue(7), 7);
  assert.equal(maskValue('plain'), 'plain');
});

test('the original object is not mutated', () => {
  const input = { password: 'secret-value' };
  maskValue(input);
  assert.equal(input.password, 'secret-value');
});

Run and verify

node --test mask.test.mjs
node mask.mjs payload.json

Run the tests for nested and array shapes, then mask a sample payload file from the CLI.

Expected Output

The test run should print three passing tests covering nested objects, arrays, and non-mutation of the input. The CLI’s console.log output should print the masked document with 2-space indentation — sensitive values replaced by the literal *** while safe fields stay intact. Given a payload holding a password and a nested apiToken, the Output: is:

{
  "user": "amy",
  "password": "***",
  "nested": {
    "apiKey": "***",
    "safe": "keep"
  }
}

Masking is applied at any depth and inside arrays, and because the function builds a new structure the original document on disk is never mutated.

Limitations

Masking is keyed on names only, so a secret stored under an innocent key passes through untouched.