UNPKG

@unito/integration-sdk

Version:

Integration SDK

114 lines (113 loc) 3.9 kB
const WEBHOOK_FILE_REGEX = /webhook.*\.ts$/i; const WEBHOOK_HANDLER_TYPES = new Set([ 'ParseWebhookHandler', 'ParseWebhooksHandler', 'AcknowledgeWebhookHandler', 'AcknowledgeWebhooksHandler', ]); function isJsonParseCall(node) { if (node.type !== 'CallExpression') { return false; } const callee = node.callee; if (!callee || callee.type !== 'MemberExpression') { return false; } if (!callee.object || callee.object.type !== 'Identifier' || callee.object.name !== 'JSON') { return false; } if (callee.property.type === 'Identifier' && callee.property.name === 'parse') { return true; } if (callee.property.type === 'Literal' && callee.property.value === 'parse') { return true; } return false; } // Function boundaries are NOT a hard stop: an arrow callback synchronously // invoked inside `try { arr.map((r) => JSON.parse(r)) }` is genuinely covered. // A function that itself contains no try will walk to Program and return false. function isCoveredByTryOrCatch(node) { let current = node; let parent = current.parent; while (parent) { if (parent.type === 'CatchClause') { return true; } if (parent.type === 'TryStatement') { const tryStmt = parent; if (tryStmt.block === current) { return true; } } current = parent; parent = parent.parent; } return false; } // Detect arrow/function expression assigned to a variable typed as one of // the webhook handler types from integration-sdk: // const parseWebhooks: ParseWebhooksHandler = async (ctx) => { ... } function isWebhookHandlerFunction(fnNode) { const parent = fnNode.parent; if (parent?.type !== 'VariableDeclarator') { return false; } const declarator = parent; const id = declarator.id; if (id.type !== 'Identifier' || !id.typeAnnotation) { return false; } const ta = id.typeAnnotation.typeAnnotation; if (ta.type !== 'TSTypeReference' || !ta.typeName || ta.typeName.type !== 'Identifier') { return false; } return WEBHOOK_HANDLER_TYPES.has(ta.typeName.name); } function isInsideWebhookHandler(node) { let p = node.parent; while (p) { if (p.type === 'ArrowFunctionExpression' || p.type === 'FunctionExpression') { if (isWebhookHandlerFunction(p)) { return true; } } p = p.parent; } return false; } const rule = { meta: { type: 'problem', docs: { description: 'In webhook files, JSON.parse must be wrapped in a try block — webhook payloads are untrusted and JSON.parse throws crash the handler.', recommended: true, }, schema: [], messages: { missingTry: 'JSON.parse on a webhook payload must be wrapped in a try block. Webhook bodies are provider-controlled untrusted strings; an unhandled throw returns 500, the provider retries per its bounded policy, and the event is eventually dropped — silent data loss. Wrap the call in `try { ... } catch (e) { logger.warn(...); ... }`.', }, }, create(context) { const filename = context.filename ?? ''; const fileMatches = WEBHOOK_FILE_REGEX.test(filename); return { CallExpression(node) { if (!isJsonParseCall(node)) { return; } if (!fileMatches && !isInsideWebhookHandler(node)) { return; } if (isCoveredByTryOrCatch(node)) { return; } context.report({ node, messageId: 'missingTry', }); }, }; }, }; export default rule;