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.
Project
node-json-mask
Entry point: mask.mjs
Install
node --versionRun
node mask.mjs payload.jsonTest
node --test mask.test.mjsExpected 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.