86 lines
2.8 KiB
JavaScript
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,
|
|
};
|