verae-middleware/src/debug/redact.js

93 lines
2.3 KiB
JavaScript

/**
* @fileoverview Secret redaction for debug logs.
* @module debug/redact
*/
const SENSITIVE_KEYS = new Set([
'password',
'veraepassword',
'token',
'veraetoken',
'accesstoken',
'apikey',
'api_key',
'secret',
'authorization',
'x-api-key',
'cookie',
'sessionkey',
]);
const BEARER_RE = /^Bearer\s+.+/i;
const API_KEY_RE = /^zmw_[A-Za-z0-9_-]+/;
const SESSION_RE = /^zmt_[A-Za-z0-9_.-]+/;
const JWT_RE = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/;
/**
* Redact a single string value if it looks like a secret.
*
* @param {string} value - Raw string.
* @returns {string} Original or `[REDACTED]`.
*/
export function redactString(value) {
if (typeof value !== 'string') return value;
if (
BEARER_RE.test(value) ||
API_KEY_RE.test(value) ||
SESSION_RE.test(value) ||
JWT_RE.test(value)
) {
return '[REDACTED]';
}
return value;
}
/**
* Deep-clone plain data structures while redacting sensitive keys and values.
*
* @param {unknown} input - Any JSON-like value.
* @param {number} [depth=0] - Recursion depth guard.
* @returns {unknown} Redacted structure safe for logging.
*
* @example
* redact({ password: 'x', jobId: '1' }); // { password: '[REDACTED]', jobId: '1' }
*/
export function redact(input, depth = 0) {
if (depth > 8) return '[MaxDepth]';
if (input === null || input === undefined) return input;
if (typeof input === 'string') return redactString(input);
if (typeof input === 'number' || typeof input === 'boolean') return input;
if (input instanceof Error) {
return { name: input.name, message: redactString(input.message), stack: undefined };
}
if (Array.isArray(input)) {
return input.map((item) => redact(item, depth + 1));
}
if (typeof input === 'object') {
/** @type {Record<string, unknown>} */
const out = {};
for (const [key, value] of Object.entries(input)) {
if (SENSITIVE_KEYS.has(key.toLowerCase())) {
out[key] = '[REDACTED]';
} else if (key.toLowerCase() === 'targeturl' && typeof value === 'string') {
try {
const u = new URL(value);
out[key] = `${u.origin}${u.pathname}`;
} catch {
out[key] = '[REDACTED_URL]';
}
} else {
out[key] = redact(value, depth + 1);
}
}
return out;
}
return String(input);
}