@unito/integration-sdk
Version:
Integration SDK
104 lines (103 loc) • 4.06 kB
JavaScript
import { isInsideCatchClause, isHttpErrorsCallee } from '../utils.js';
const SCOPE_FILE_RE = /\/src\/(handlers|routes)\//;
const RATE_LIMITER_RE = /[Rr]ate[Ll]imit/;
const ERROR_IDENTIFIER_RE = /^(err|error|e|originalError|cause)$/i;
function getPropertyName(property) {
if (!property) {
return null;
}
if (property.type === 'Identifier') {
return property.name;
}
if (property.type === 'Literal' && typeof property.value === 'string') {
return property.value;
}
return null;
}
function isErrorMessageAccess(memberExpression) {
if (memberExpression.type !== 'MemberExpression') {
return false;
}
if (getPropertyName(memberExpression.property) !== 'message') {
return false;
}
if (memberExpression.object.type !== 'Identifier') {
return false;
}
return ERROR_IDENTIFIER_RE.test(memberExpression.object.name);
}
function containsErrorMessageAccess(node) {
if (!node) {
return false;
}
if (node.type === 'MemberExpression') {
if (isErrorMessageAccess(node)) {
return true;
}
return containsErrorMessageAccess(node.object) || containsErrorMessageAccess(node.property);
}
if (node.type === 'TemplateLiteral') {
return node.expressions.some(e => containsErrorMessageAccess(e));
}
if (node.type === 'ConditionalExpression') {
return (containsErrorMessageAccess(node.test) ||
containsErrorMessageAccess(node.consequent) ||
containsErrorMessageAccess(node.alternate));
}
if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
return containsErrorMessageAccess(node.left) || containsErrorMessageAccess(node.right);
}
if (node.type === 'CallExpression' || node.type === 'NewExpression') {
return node.arguments.some(arg => containsErrorMessageAccess(arg));
}
// TS-specific node types (cast / non-null assertion). `estree` doesn't declare
// them but @typescript-eslint/parser emits them; treat them as transparent
// wrappers and recurse on .expression.
const ts = node;
if (ts.type === 'TSAsExpression' || ts.type === 'TSNonNullExpression') {
return containsErrorMessageAccess(ts.expression);
}
return false;
}
const rule = {
meta: {
type: 'problem',
docs: {
description: 'Disallow rethrowing the provider error message into the HTTP response (`HttpErrors.X(err.message)`); strip the provider message and throw a stable summary across the boundary',
recommended: true,
},
schema: [],
messages: {
errorMessageRethrow: 'Do not propagate `error.message` (or `err.message`/`e.message`) into a `HttpErrors.*` throw. Provider error strings can carry tokens, PII, or internal field names — rethrowing them across the HTTP boundary turns the connector into an exfiltration path. Log the original message via the structured logger (key-based redaction) and throw a stable, provider-agnostic summary instead.',
},
},
create(context) {
const filename = context.filename ?? '';
if (!SCOPE_FILE_RE.test(filename)) {
return {};
}
if (RATE_LIMITER_RE.test(filename)) {
return {};
}
return {
ThrowStatement(node) {
const argument = node.argument;
if (!argument || argument.type !== 'NewExpression') {
return;
}
if (!isHttpErrorsCallee(argument.callee)) {
return;
}
if (!isInsideCatchClause(node)) {
return;
}
const propagates = argument.arguments.some(arg => containsErrorMessageAccess(arg));
if (!propagates) {
return;
}
context.report({ node, messageId: 'errorMessageRethrow' });
},
};
},
};
export default rule;