UNPKG

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
// 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 };