Files
Alta-Proxy-Tool/src/error-redaction.js
T

86 lines
2.8 KiB
JavaScript

'use strict';
const MAX_OPERATION_LENGTH = 64;
const MAX_CODE_LENGTH = 64;
const MAX_MESSAGE_LENGTH = 240;
const REDACTED = '[REDACTED]';
function safeRead(object, key) {
try {
return object && typeof object === 'object' ? object[key] : undefined;
} catch {
return undefined;
}
}
function redactKnownPatterns(message) {
return message
.replace(/\bauthorization\s*:\s*bearer\s+[^\s,;]+/gi, REDACTED)
.replace(/\bbearer\s+[^\s,;]+/gi, `Bearer ${REDACTED}`)
.replace(/\bcookie\s*:\s*[^\r\n]*/gi, REDACTED)
.replace(/\b(va\s*=\s*)[^\s;,]*/gi, `$1${REDACTED}`);
}
function sanitizeErrorMessage(value, options = {}) {
let message = typeof value === 'string' ? value : 'Alta request failed';
const secrets = Array.isArray(options.secrets) ? options.secrets : [];
for (const secret of secrets) {
if (typeof secret === 'string' && secret.length > 0) {
message = message.split(secret).join(REDACTED);
}
}
message = redactKnownPatterns(message)
.replace(/[\r\n\t]+/g, ' ')
.replace(/[\u0000-\u001f\u007f]/g, '')
.replace(/\s{2,}/g, ' ')
.trim();
if (!message) message = 'Alta request failed';
return message.slice(0, MAX_MESSAGE_LENGTH);
}
function includesSecret(value, secrets) {
return secrets.some((secret) => typeof secret === 'string' && secret.length > 0 && value.includes(secret));
}
function safeIdentifier(value, fallback, maxLength, secrets) {
if (typeof value !== 'string' || !/^[A-Za-z0-9_.:-]+$/.test(value)) return fallback;
if (includesSecret(value, secrets)) return fallback;
return value.slice(0, maxLength);
}
function safeStatus(error) {
const response = safeRead(error, 'response');
const candidate = safeRead(response, 'status') ?? safeRead(error, 'status');
return Number.isInteger(candidate) && candidate >= 100 && candidate <= 599 ? candidate : null;
}
function formatAltaError(operation, error, options = {}) {
const secrets = Array.isArray(options.secrets) ? options.secrets : [];
const rawMessage = safeRead(error, 'message');
const rawCode = safeRead(error, 'code');
const timeoutValue = safeRead(error, 'timeout');
const timeout = timeoutValue === true || rawCode === 'ECONNABORTED' || rawCode === 'ETIMEDOUT' || rawCode === 'ALTA_TIMEOUT';
// Deliberately construct a fresh allowlisted object. Never copy or spread the
// source error: Axios config/request/headers/data can carry the bearer cookie.
return Object.freeze({
operation: safeIdentifier(operation, 'altaRequest', MAX_OPERATION_LENGTH, secrets),
code: safeIdentifier(rawCode, 'ALTA_ERROR', MAX_CODE_LENGTH, secrets),
status: safeStatus(error),
timeout,
message: sanitizeErrorMessage(rawMessage, options),
});
}
module.exports = {
MAX_CODE_LENGTH,
MAX_MESSAGE_LENGTH,
MAX_OPERATION_LENGTH,
formatAltaError,
formatError: formatAltaError,
sanitizeErrorMessage,
};