hook-engine
Version:
Production-grade webhook engine with comprehensive adapter support, security, reliability, structured logging, and CLI tools.
162 lines (161 loc) • 6.52 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGenericAdapter = createGenericAdapter;
const crypto_1 = __importDefault(require("crypto"));
/**
* Generic webhook adapter
* Supports: custom webhook formats with configurable signature verification
* Use this for any webhook provider not specifically supported
*/
function createGenericAdapter(config = {}) {
const { signatureHeader = 'x-signature', signatureAlgorithm = 'sha256', signatureEncoding = 'hex', signaturePrefix = '', timestampHeader = 'x-timestamp', timestampValidationWindow = 300, // 5 minutes
payloadFormat = 'json', eventTypeField = 'event', eventIdField = 'id', timestampField = 'timestamp' } = config;
return {
getSignature(req) {
return req.headers[signatureHeader.toLowerCase()];
},
verifySignature(rawBody, signature, secret) {
if (!signature)
return false;
try {
// Remove prefix if specified
let cleanSignature = signature;
if (signaturePrefix && signature.startsWith(signaturePrefix)) {
cleanSignature = signature.slice(signaturePrefix.length);
}
// Generate expected signature
const hmac = crypto_1.default.createHmac(signatureAlgorithm, secret);
hmac.update(rawBody);
const expected = hmac.digest(signatureEncoding);
// Compare signatures
return crypto_1.default.timingSafeEqual(Buffer.from(expected), Buffer.from(cleanSignature));
}
catch (error) {
console.error('Generic adapter signature verification error:', error);
return false;
}
},
parsePayload(body) {
const bodyString = body.toString("utf8");
switch (payloadFormat) {
case 'json':
try {
return JSON.parse(bodyString);
}
catch {
throw new Error('Invalid JSON payload');
}
case 'form':
const params = {};
const pairs = bodyString.split('&');
for (const pair of pairs) {
const [key, value] = pair.split('=');
if (key && value !== undefined) {
params[decodeURIComponent(key)] = decodeURIComponent(value);
}
}
return params;
case 'raw':
return { rawData: bodyString };
default:
throw new Error(`Unsupported payload format: ${payloadFormat}`);
}
},
normalize(event, options) {
let type = 'unknown';
let id = '';
let timestamp = Math.floor(Date.now() / 1000);
let payload = {};
// Extract event type
if (eventTypeField && event[eventTypeField]) {
type = `generic.${event[eventTypeField]}`;
}
else if (event.type) {
type = `generic.${event.type}`;
}
else if (event.event_type) {
type = `generic.${event.event_type}`;
}
else {
type = 'generic.unknown';
}
// Extract event ID
if (eventIdField && event[eventIdField]) {
id = event[eventIdField].toString();
}
else if (event.id) {
id = event.id.toString();
}
else if (event.event_id) {
id = event.event_id.toString();
}
else if (event.uuid) {
id = event.uuid.toString();
}
else {
id = `generic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Extract timestamp
if (timestampField && event[timestampField]) {
const ts = event[timestampField];
if (typeof ts === 'number') {
// Assume Unix timestamp
timestamp = ts > 1e10 ? Math.floor(ts / 1000) : ts;
}
else if (typeof ts === 'string') {
// Try to parse as ISO date
const parsed = new Date(ts);
if (!isNaN(parsed.getTime())) {
timestamp = Math.floor(parsed.getTime() / 1000);
}
}
}
else if (event.timestamp) {
const ts = event.timestamp;
if (typeof ts === 'number') {
timestamp = ts > 1e10 ? Math.floor(ts / 1000) : ts;
}
else if (typeof ts === 'string') {
const parsed = new Date(ts);
if (!isNaN(parsed.getTime())) {
timestamp = Math.floor(parsed.getTime() / 1000);
}
}
}
else if (event.created_at) {
const parsed = new Date(event.created_at);
if (!isNaN(parsed.getTime())) {
timestamp = Math.floor(parsed.getTime() / 1000);
}
}
else if (event.occurred_at) {
const parsed = new Date(event.occurred_at);
if (!isNaN(parsed.getTime())) {
timestamp = Math.floor(parsed.getTime() / 1000);
}
}
// Use the entire event as payload, but clean up some fields
payload = { ...event };
// Remove fields that are now in the normalized structure
delete payload[eventTypeField];
delete payload[eventIdField];
delete payload[timestampField];
return {
id,
type,
source: "generic",
timestamp,
payload,
raw: options?.includeRaw ? JSON.stringify(event) : '',
};
},
};
}
/**
* Default generic adapter with common settings
*/
const generic = createGenericAdapter();
exports.default = generic;