sanitize-data
Version:
🧼 A lightweight utility for sanitization, redacting, masking, and randomizing sensitive or structured data in JavaScript/TypeScript.
256 lines (255 loc) • 8.85 kB
JavaScript
// src/index.ts
function parsePath(path) {
const parts = [];
let currentPart = "";
let inBrackets = false;
for (let i = 0; i < path.length; i++) {
const char = path[i];
if (char === "." && !inBrackets) {
if (currentPart) {
parts.push(currentPart);
currentPart = "";
}
} else if (char === "[" && !inBrackets) {
if (currentPart) {
parts.push(currentPart);
currentPart = "";
}
inBrackets = true;
} else if (char === "]" && inBrackets) {
parts.push(currentPart);
currentPart = "";
inBrackets = false;
} else {
currentPart += char;
}
}
if (currentPart) {
parts.push(currentPart);
}
return parts;
}
function isWildcardMatch(pathParts, patternParts, alwyasFullPath) {
let i = 0, j = 0;
while (i < pathParts.length && j < patternParts.length) {
if (patternParts[j] === "**") {
if (j === patternParts.length - 1)
return true;
while (i < pathParts.length) {
if (isWildcardMatch(pathParts.slice(i), patternParts.slice(j + 1), alwyasFullPath))
return true;
i++;
}
return false;
} else if (patternParts[j] === "*") {
i++;
j++;
} else if (patternParts[j] === pathParts[i]) {
i++;
j++;
} else {
return false;
}
}
if (patternParts.length === 2 && patternParts[0] === "*") {
if (pathParts.length === 1 && alwyasFullPath.length === 1 && pathParts[0] === patternParts[1]) {
return true;
}
return false;
}
return i === pathParts.length && j === patternParts.length;
}
function findMostSpecificRule(path, parsedPath, rules, keyMatchAnyLevel = true) {
let bestMatch;
for (const ruleKey of Object.keys(rules)) {
const ruleParts = parsePath(ruleKey);
const normalizedRuleParts = ruleParts.length === 1 ? ["**", ...ruleParts] : ruleParts;
if (isWildcardMatch(parsedPath, normalizedRuleParts, parsedPath)) {
const specificity = calculateSpecificity(normalizedRuleParts);
if (!bestMatch || specificity > bestMatch.specificity) {
bestMatch = { specificity, mode: rules[ruleKey] };
}
}
}
return bestMatch?.mode;
}
function calculateSpecificity(patternParts) {
return patternParts.reduce((score, part) => {
if (part === "**")
return score + 1;
if (part === "*")
return score + 10;
return score + 100;
}, 0);
}
function sanitize(input, options) {
const {
rules = {},
defaultMode = "preserve",
redactString = "[REDACTED]",
randomString = "[random]",
randomGenerators = {},
randomFieldGenerators = {},
randomFieldGeneratorsCaseInsensitive = false,
keyMatchAnyLevel = true
} = options;
let randomFieldGeneratorsLower;
if (randomFieldGeneratorsCaseInsensitive) {
randomFieldGeneratorsLower = {};
for (const k of Object.keys(randomFieldGenerators)) {
randomFieldGeneratorsLower[k.toLowerCase()] = randomFieldGenerators[k];
}
}
const seen = /* @__PURE__ */ new WeakMap();
function findMatchingRandomFieldGenerator(path, parsedPath) {
if (randomFieldGenerators[path]) {
return randomFieldGenerators[path];
}
if (randomFieldGeneratorsCaseInsensitive) {
const lowerPath = path.toLowerCase();
if (randomFieldGeneratorsLower && randomFieldGeneratorsLower[lowerPath]) {
return randomFieldGeneratorsLower[lowerPath];
}
for (const genKey of Object.keys(randomFieldGenerators)) {
if (genKey.toLowerCase() === lowerPath) {
return randomFieldGenerators[genKey];
}
}
}
let bestMatch;
const checkAndUpdateBestMatch = (genKey, generator, isCaseInsensitive) => {
const genKeyParsed = parsePath(genKey);
const normalizedGenKeyParsed = keyMatchAnyLevel && genKeyParsed.length === 1 ? ["**", ...genKeyParsed] : genKeyParsed;
const match = isCaseInsensitive ? isWildcardMatchCaseInsensitive(parsedPath, normalizedGenKeyParsed, parsedPath) : isWildcardMatch(parsedPath, normalizedGenKeyParsed, parsedPath);
if (match) {
const specificity = calculateSpecificity(normalizedGenKeyParsed);
if (!bestMatch || specificity > bestMatch.specificity) {
bestMatch = { specificity, generator };
}
}
};
for (const genKey of Object.keys(randomFieldGenerators)) {
checkAndUpdateBestMatch(genKey, randomFieldGenerators[genKey], false);
}
if (randomFieldGeneratorsCaseInsensitive) {
for (const genKey of Object.keys(randomFieldGenerators)) {
checkAndUpdateBestMatch(genKey, randomFieldGenerators[genKey], true);
}
}
return bestMatch?.generator;
}
function isWildcardMatchCaseInsensitive(pathParts, patternParts, alwaysFullPath) {
const lowerPathParts = pathParts.map((part) => part.toLowerCase());
const lowerPatternParts = patternParts.map((part) => part === "*" || part === "**" ? part : part.toLowerCase());
const lowerFullPath = alwaysFullPath.map((part) => part.toLowerCase());
return isWildcardMatch(lowerPathParts, lowerPatternParts, lowerFullPath);
}
const walk = (obj, path = []) => {
if (obj === null || typeof obj !== "object")
return obj;
if (seen.has(obj)) {
return seen.get(obj);
}
const result = Array.isArray(obj) ? [] : {};
seen.set(obj, result);
const currentPath = path.join(".");
const isArrayWithDirectRule = Array.isArray(obj) && rules[currentPath] === "random";
if (isArrayWithDirectRule) {
return randomValue(obj, randomString, randomGenerators);
}
for (const key of Object.keys(obj)) {
const fullPath = [...path, key].join(".");
const parsedPath = parsePath(fullPath);
let mode = findMostSpecificRule(
parsedPath.join("."),
parsedPath,
rules,
keyMatchAnyLevel
);
if (!mode)
mode = defaultMode;
const value = obj[key];
const fieldGen = findMatchingRandomFieldGenerator(fullPath, parsedPath);
if (fieldGen) {
result[key] = fieldGen(value, fullPath);
continue;
}
switch (mode) {
case "mask":
if (value !== null && typeof value === "object") {
result[key] = walk(value, [...path, key]);
} else {
result[key] = maskValue(value);
}
break;
case "redact":
result[key] = redactString;
break;
case "random": {
if (value !== null && typeof value === "object") {
if (randomGenerators.object && !Array.isArray(value)) {
if (Object.keys(randomFieldGenerators).length > 0) {
result[key] = walk(value, [...path, key]);
} else {
result[key] = randomGenerators.object(value);
}
} else if (randomGenerators.array && Array.isArray(value)) {
result[key] = randomGenerators.array(value);
} else {
result[key] = walk(value, [...path, key]);
}
} else {
result[key] = randomValue(value, randomString, randomGenerators);
}
break;
}
case "preserve":
default:
result[key] = typeof value === "object" ? walk(value, [...path, key]) : value;
}
}
return result;
};
return walk(input);
}
function maskValue(value) {
const str = String(value);
return "*".repeat(Math.min(8, str.length));
}
function randomValue(value, randomString = "[random]", randomGenerators = {}) {
if (typeof value === "number" && randomGenerators.number)
return randomGenerators.number();
if (typeof value === "string" && randomGenerators.string)
return randomGenerators.string();
if (typeof value === "boolean" && randomGenerators.boolean)
return randomGenerators.boolean();
if (Array.isArray(value) && randomGenerators.array)
return randomGenerators.array(value);
if (typeof value === "object" && value !== null && randomGenerators.object)
return randomGenerators.object(value);
if (typeof value === "number")
return Math.floor(Math.random() * 1e4);
if (typeof value === "string")
return Math.random().toString(36).slice(2, 10);
if (typeof value === "boolean")
return Math.random() > 0.5;
if (Array.isArray(value)) {
return value.map((item) => {
if (typeof item === "number")
return Math.floor(Math.random() * 1e4);
if (typeof item === "string")
return Math.random().toString(36).slice(2, 10);
if (typeof item === "boolean")
return Math.random() > 0.5;
if (item === null || item === void 0)
return randomString;
if (typeof item === "object")
return randomString;
return randomString;
});
}
return randomString;
}
export {
sanitize
};