mmpSearch/node_modules/@pinojs/redact/index.js

530 lines
15 KiB
JavaScript

'use strict'
function deepClone (obj) {
if (obj === null || typeof obj !== 'object') {
return obj
}
if (obj instanceof Date) {
return new Date(obj.getTime())
}
if (obj instanceof Array) {
const cloned = []
for (let i = 0; i < obj.length; i++) {
cloned[i] = deepClone(obj[i])
}
return cloned
}
if (typeof obj === 'object') {
const cloned = Object.create(Object.getPrototypeOf(obj))
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key])
}
}
return cloned
}
return obj
}
function parsePath (path) {
const parts = []
let current = ''
let inBrackets = false
let inQuotes = false
let quoteChar = ''
for (let i = 0; i < path.length; i++) {
const char = path[i]
if (!inBrackets && char === '.') {
if (current) {
parts.push(current)
current = ''
}
} else if (char === '[') {
if (current) {
parts.push(current)
current = ''
}
inBrackets = true
} else if (char === ']' && inBrackets) {
// Always push the current value when closing brackets, even if it's an empty string
parts.push(current)
current = ''
inBrackets = false
inQuotes = false
} else if ((char === '"' || char === "'") && inBrackets) {
if (!inQuotes) {
inQuotes = true
quoteChar = char
} else if (char === quoteChar) {
inQuotes = false
quoteChar = ''
} else {
current += char
}
} else {
current += char
}
}
if (current) {
parts.push(current)
}
return parts
}
function setValue (obj, parts, value) {
let current = obj
for (let i = 0; i < parts.length - 1; i++) {
const key = parts[i]
// Type safety: Check if current is an object before using 'in' operator
if (typeof current !== 'object' || current === null || !(key in current)) {
return false // Path doesn't exist, don't create it
}
if (typeof current[key] !== 'object' || current[key] === null) {
return false // Path doesn't exist properly
}
current = current[key]
}
const lastKey = parts[parts.length - 1]
if (lastKey === '*') {
if (Array.isArray(current)) {
for (let i = 0; i < current.length; i++) {
current[i] = value
}
} else if (typeof current === 'object' && current !== null) {
for (const key in current) {
if (Object.prototype.hasOwnProperty.call(current, key)) {
current[key] = value
}
}
}
} else {
// Type safety: Check if current is an object before using 'in' operator
if (typeof current === 'object' && current !== null && lastKey in current && Object.prototype.hasOwnProperty.call(current, lastKey)) {
current[lastKey] = value
}
}
return true
}
function removeKey (obj, parts) {
let current = obj
for (let i = 0; i < parts.length - 1; i++) {
const key = parts[i]
// Type safety: Check if current is an object before using 'in' operator
if (typeof current !== 'object' || current === null || !(key in current)) {
return false // Path doesn't exist, don't create it
}
if (typeof current[key] !== 'object' || current[key] === null) {
return false // Path doesn't exist properly
}
current = current[key]
}
const lastKey = parts[parts.length - 1]
if (lastKey === '*') {
if (Array.isArray(current)) {
// For arrays, we can't really "remove" all items as that would change indices
// Instead, we set them to undefined which will be omitted by JSON.stringify
for (let i = 0; i < current.length; i++) {
current[i] = undefined
}
} else if (typeof current === 'object' && current !== null) {
for (const key in current) {
if (Object.prototype.hasOwnProperty.call(current, key)) {
delete current[key]
}
}
}
} else {
// Type safety: Check if current is an object before using 'in' operator
if (typeof current === 'object' && current !== null && lastKey in current && Object.prototype.hasOwnProperty.call(current, lastKey)) {
delete current[lastKey]
}
}
return true
}
// Sentinel object to distinguish between undefined value and non-existent path
const PATH_NOT_FOUND = Symbol('PATH_NOT_FOUND')
function getValueIfExists (obj, parts) {
let current = obj
for (const part of parts) {
if (current === null || current === undefined) {
return PATH_NOT_FOUND
}
// Type safety: Check if current is an object before property access
if (typeof current !== 'object' || current === null) {
return PATH_NOT_FOUND
}
// Check if the property exists before accessing it
if (!(part in current)) {
return PATH_NOT_FOUND
}
current = current[part]
}
return current
}
function getValue (obj, parts) {
let current = obj
for (const part of parts) {
if (current === null || current === undefined) {
return undefined
}
// Type safety: Check if current is an object before property access
if (typeof current !== 'object' || current === null) {
return undefined
}
current = current[part]
}
return current
}
function redactPaths (obj, paths, censor, remove = false) {
for (const path of paths) {
const parts = parsePath(path)
if (parts.includes('*')) {
redactWildcardPath(obj, parts, censor, path, remove)
} else {
if (remove) {
removeKey(obj, parts)
} else {
// Get value only if path exists - single traversal
const value = getValueIfExists(obj, parts)
if (value === PATH_NOT_FOUND) {
continue
}
const actualCensor = typeof censor === 'function'
? censor(value, parts)
: censor
setValue(obj, parts, actualCensor)
}
}
}
}
function redactWildcardPath (obj, parts, censor, originalPath, remove = false) {
const wildcardIndex = parts.indexOf('*')
if (wildcardIndex === parts.length - 1) {
const parentParts = parts.slice(0, -1)
let current = obj
for (const part of parentParts) {
if (current === null || current === undefined) return
// Type safety: Check if current is an object before property access
if (typeof current !== 'object' || current === null) return
current = current[part]
}
if (Array.isArray(current)) {
if (remove) {
// For arrays, set all items to undefined which will be omitted by JSON.stringify
for (let i = 0; i < current.length; i++) {
current[i] = undefined
}
} else {
for (let i = 0; i < current.length; i++) {
const indexPath = [...parentParts, i.toString()]
const actualCensor = typeof censor === 'function'
? censor(current[i], indexPath)
: censor
current[i] = actualCensor
}
}
} else if (typeof current === 'object' && current !== null) {
if (remove) {
// Collect keys to delete to avoid issues with deleting during iteration
const keysToDelete = []
for (const key in current) {
if (Object.prototype.hasOwnProperty.call(current, key)) {
keysToDelete.push(key)
}
}
for (const key of keysToDelete) {
delete current[key]
}
} else {
for (const key in current) {
const keyPath = [...parentParts, key]
const actualCensor = typeof censor === 'function'
? censor(current[key], keyPath)
: censor
current[key] = actualCensor
}
}
}
} else {
redactIntermediateWildcard(obj, parts, censor, wildcardIndex, originalPath, remove)
}
}
function redactIntermediateWildcard (obj, parts, censor, wildcardIndex, originalPath, remove = false) {
const beforeWildcard = parts.slice(0, wildcardIndex)
const afterWildcard = parts.slice(wildcardIndex + 1)
const pathArray = [] // Cached array to avoid allocations
function traverse (current, pathLength) {
if (pathLength === beforeWildcard.length) {
if (Array.isArray(current)) {
for (let i = 0; i < current.length; i++) {
pathArray[pathLength] = i.toString()
traverse(current[i], pathLength + 1)
}
} else if (typeof current === 'object' && current !== null) {
for (const key in current) {
pathArray[pathLength] = key
traverse(current[key], pathLength + 1)
}
}
} else if (pathLength < beforeWildcard.length) {
const nextKey = beforeWildcard[pathLength]
// Type safety: Check if current is an object before using 'in' operator
if (current && typeof current === 'object' && current !== null && nextKey in current) {
pathArray[pathLength] = nextKey
traverse(current[nextKey], pathLength + 1)
}
} else {
// Check if afterWildcard contains more wildcards
if (afterWildcard.includes('*')) {
// Recursively handle remaining wildcards
// Wrap censor to prepend current path context
const wrappedCensor = typeof censor === 'function'
? (value, path) => {
const fullPath = [...pathArray.slice(0, pathLength), ...path]
return censor(value, fullPath)
}
: censor
redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove)
} else {
// No more wildcards, apply the redaction directly
if (remove) {
removeKey(current, afterWildcard)
} else {
const actualCensor = typeof censor === 'function'
? censor(getValue(current, afterWildcard), [...pathArray.slice(0, pathLength), ...afterWildcard])
: censor
setValue(current, afterWildcard, actualCensor)
}
}
}
}
if (beforeWildcard.length === 0) {
traverse(obj, 0)
} else {
let current = obj
for (let i = 0; i < beforeWildcard.length; i++) {
const part = beforeWildcard[i]
if (current === null || current === undefined) return
// Type safety: Check if current is an object before property access
if (typeof current !== 'object' || current === null) return
current = current[part]
pathArray[i] = part
}
if (current !== null && current !== undefined) {
traverse(current, beforeWildcard.length)
}
}
}
function buildPathStructure (pathsToClone) {
if (pathsToClone.length === 0) {
return null // No paths to redact
}
// Parse all paths and organize by depth
const pathStructure = new Map()
for (const path of pathsToClone) {
const parts = parsePath(path)
let current = pathStructure
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
if (!current.has(part)) {
current.set(part, new Map())
}
current = current.get(part)
}
}
return pathStructure
}
function selectiveClone (obj, pathStructure) {
if (!pathStructure) {
return obj // No paths to redact, return original
}
function cloneSelectively (source, pathMap, depth = 0) {
if (!pathMap || pathMap.size === 0) {
return source // No more paths to clone, return reference
}
if (source === null || typeof source !== 'object') {
return source
}
if (source instanceof Date) {
return new Date(source.getTime())
}
if (Array.isArray(source)) {
const cloned = []
for (let i = 0; i < source.length; i++) {
const indexStr = i.toString()
if (pathMap.has(indexStr) || pathMap.has('*')) {
cloned[i] = cloneSelectively(source[i], pathMap.get(indexStr) || pathMap.get('*'))
} else {
cloned[i] = source[i] // Share reference for non-redacted items
}
}
return cloned
}
// Handle objects
const cloned = Object.create(Object.getPrototypeOf(source))
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (pathMap.has(key) || pathMap.has('*')) {
cloned[key] = cloneSelectively(source[key], pathMap.get(key) || pathMap.get('*'))
} else {
cloned[key] = source[key] // Share reference for non-redacted properties
}
}
}
return cloned
}
return cloneSelectively(obj, pathStructure)
}
function validatePath (path) {
if (typeof path !== 'string') {
throw new Error('Paths must be (non-empty) strings')
}
if (path === '') {
throw new Error('Invalid redaction path ()')
}
// Check for double dots
if (path.includes('..')) {
throw new Error(`Invalid redaction path (${path})`)
}
// Check for comma-separated paths (invalid syntax)
if (path.includes(',')) {
throw new Error(`Invalid redaction path (${path})`)
}
// Check for unmatched brackets
let bracketCount = 0
let inQuotes = false
let quoteChar = ''
for (let i = 0; i < path.length; i++) {
const char = path[i]
if ((char === '"' || char === "'") && bracketCount > 0) {
if (!inQuotes) {
inQuotes = true
quoteChar = char
} else if (char === quoteChar) {
inQuotes = false
quoteChar = ''
}
} else if (char === '[' && !inQuotes) {
bracketCount++
} else if (char === ']' && !inQuotes) {
bracketCount--
if (bracketCount < 0) {
throw new Error(`Invalid redaction path (${path})`)
}
}
}
if (bracketCount !== 0) {
throw new Error(`Invalid redaction path (${path})`)
}
}
function validatePaths (paths) {
if (!Array.isArray(paths)) {
throw new TypeError('paths must be an array')
}
for (const path of paths) {
validatePath(path)
}
}
function slowRedact (options = {}) {
const {
paths = [],
censor = '[REDACTED]',
serialize = JSON.stringify,
strict = true,
remove = false
} = options
// Validate paths upfront to match fast-redact behavior
validatePaths(paths)
// Build path structure once during setup, not on every call
const pathStructure = buildPathStructure(paths)
return function redact (obj) {
if (strict && (obj === null || typeof obj !== 'object')) {
if (obj === null || obj === undefined) {
return serialize ? serialize(obj) : obj
}
if (typeof obj !== 'object') {
return serialize ? serialize(obj) : obj
}
}
// Only clone paths that need redaction
const cloned = selectiveClone(obj, pathStructure)
const original = obj // Keep reference to original for restore
let actualCensor = censor
if (typeof censor === 'function') {
actualCensor = censor
}
redactPaths(cloned, paths, actualCensor, remove)
if (serialize === false) {
cloned.restore = function () {
return deepClone(original) // Full clone only when restore is called
}
return cloned
}
if (typeof serialize === 'function') {
return serialize(cloned)
}
return JSON.stringify(cloned)
}
}
module.exports = slowRedact