@logtape/redaction
Version:
Redact sensitive data from log messages
139 lines (137 loc) • 5.51 kB
JavaScript
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
const require_traversal = require('./traversal.cjs');
const __logtape_logtape = require_rolldown_runtime.__toESM(require("@logtape/logtape"));
//#region src/pattern.ts
const metaLogger = (0, __logtape_logtape.getLogger)(["logtape", "meta"]);
let reportingRedactionLimit = false;
/**
* A redaction pattern for email addresses.
* @since 0.10.0
*/
const EMAIL_ADDRESS_PATTERN = {
pattern: /[\p{L}0-9.!#$%&'*+/=?^_`{|}~-]+@[\p{L}0-9](?:[\p{L}0-9-]{0,61}[\p{L}0-9])?(?:\.[\p{L}0-9](?:[\p{L}0-9-]{0,61}[\p{L}0-9])?)+/gu,
replacement: "REDACTED@EMAIL.ADDRESS"
};
/**
* A redaction pattern for credit card numbers (including American Express).
* @since 0.10.0
*/
const CREDIT_CARD_NUMBER_PATTERN = {
pattern: /(?:\d{4}-){2}(?:\d{4}-\d{4}|\d{6})/g,
replacement: "XXXX-XXXX-XXXX-XXXX"
};
/**
* A redaction pattern for U.S. Social Security numbers.
* @since 0.10.0
*/
const US_SSN_PATTERN = {
pattern: /\d{3}-\d{2}-\d{4}/g,
replacement: "XXX-XX-XXXX"
};
/**
* A redaction pattern for South Korean resident registration numbers
* (住民登錄番號).
* @since 0.10.0
*/
const KR_RRN_PATTERN = {
pattern: /\d{6}-\d{7}/g,
replacement: "XXXXXX-XXXXXXX"
};
/**
* A redaction pattern for JSON Web Tokens (JWT).
* @since 0.10.0
*/
const JWT_PATTERN = {
pattern: /eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g,
replacement: "[JWT REDACTED]"
};
/**
* Checks if a value is a built-in object that should not be recursively
* processed (e.g., Error, Date, RegExp, Map, Set, etc.).
* @param value The value to check.
* @returns `true` if the value is a built-in object, `false` otherwise.
*/
function isBuiltInObject(value) {
return value instanceof Error || value instanceof Date || value instanceof RegExp || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise || value instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && value instanceof SharedArrayBuffer || ArrayBuffer.isView(value);
}
function redactByPattern(formatter, patterns, options = {}) {
for (const { pattern } of patterns) if (!pattern.global) throw new TypeError(`Pattern ${pattern} does not have the global flag set.`);
function replaceString(str) {
for (const p of patterns) str = typeof p.replacement === "string" ? str.replaceAll(p.pattern, p.replacement) : str.replaceAll(p.pattern, p.replacement);
return str;
}
function replaceObject(object, context, depth) {
if (typeof object === "object" && object !== null) {
if (context.visited.has(object)) return context.visited.get(object);
}
if (typeof object === "string") return replaceString(object);
if (Array.isArray(object)) {
const copy = [];
const length = Math.min(object.length, context.limits.maxProperties);
copy.length = length;
context.visited.set(object, copy);
if (object.length > context.limits.maxProperties) reportLimitOnce(context, "maxProperties");
for (let i = 0; i < length; i++) {
if (!(i in object)) continue;
const item = object[i];
if (shouldTraverseValue(item) && depth + 1 > context.limits.maxDepth) {
reportLimitOnce(context, "maxDepth");
copy[i] = require_traversal.redactionTruncatedValue;
} else copy[i] = replaceObject(item, context, depth + 1);
}
return copy;
}
if (typeof object === "object" && object !== null) {
if (isBuiltInObject(object)) return object;
const redacted = {};
context.visited.set(object, redacted);
const keys = Object.keys(object);
if (keys.length > context.limits.maxProperties) reportLimitOnce(context, "maxProperties");
for (const key of keys.slice(0, context.limits.maxProperties)) {
const value = object[key];
if (shouldTraverseValue(value) && depth + 1 > context.limits.maxDepth) {
reportLimitOnce(context, "maxDepth");
redacted[key] = require_traversal.redactionTruncatedValue;
} else redacted[key] = replaceObject(value, context, depth + 1);
}
return redacted;
}
return object;
}
return (record) => {
const output = formatter(record);
if (typeof output === "string") return replaceString(output);
const context = createPatternRedactionContext(options);
if (output.length > context.limits.maxProperties) reportLimitOnce(context, "maxProperties");
return output.slice(0, context.limits.maxProperties).map((obj) => replaceObject(obj, context, 0));
};
}
function reportRedactionLimitExceeded(limit, limits) {
if (reportingRedactionLimit || typeof metaLogger.warn !== "function") return;
try {
reportingRedactionLimit = true;
metaLogger.warn("Redaction traversal exceeded {limit}; replacing or omitting remaining data to keep logging bounded.", {
limit,
...limits
});
} catch {} finally {
reportingRedactionLimit = false;
}
}
function shouldTraverseValue(value) {
return typeof value === "object" && value !== null && !isBuiltInObject(value);
}
function createPatternRedactionContext(options) {
return require_traversal.createRedactionTraversalContext(options, reportRedactionLimitExceeded);
}
function reportLimitOnce(context, limit) {
if (context.exceededLimits.has(limit)) return;
context.reportLimitExceeded(limit);
}
//#endregion
exports.CREDIT_CARD_NUMBER_PATTERN = CREDIT_CARD_NUMBER_PATTERN;
exports.EMAIL_ADDRESS_PATTERN = EMAIL_ADDRESS_PATTERN;
exports.JWT_PATTERN = JWT_PATTERN;
exports.KR_RRN_PATTERN = KR_RRN_PATTERN;
exports.US_SSN_PATTERN = US_SSN_PATTERN;
exports.redactByPattern = redactByPattern;