UNPKG

durabull

Version:

A durable workflow engine built on top of BullMQ and Redis

179 lines (178 loc) 7.31 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createWebhookRouter = exports.WebhookRouter = exports.registerWebhookWorkflow = exports.SignatureAuthStrategy = exports.TokenAuthStrategy = exports.NoneAuthStrategy = void 0; const crypto_1 = require("crypto"); const WorkflowStub_1 = require("../WorkflowStub"); const decorators_1 = require("../decorators"); const global_1 = require("../config/global"); const logger_1 = require("../runtime/logger"); const isRecord = (value) => { return typeof value === 'object' && value !== null; }; class NoneAuthStrategy { async authenticate(_request) { return true; } } exports.NoneAuthStrategy = NoneAuthStrategy; class TokenAuthStrategy { constructor(token, header = 'Authorization') { this.token = token; this.header = header; } async authenticate(request) { const headerValue = request.headers[this.header] || request.headers[this.header.toLowerCase()]; if (!headerValue) return false; if (headerValue.startsWith('Bearer ')) { return headerValue.substring(7) === this.token; } return headerValue === this.token; } } exports.TokenAuthStrategy = TokenAuthStrategy; class SignatureAuthStrategy { constructor(secret, header = 'X-Signature') { this.secret = secret; this.header = header; } async authenticate(request) { const signature = request.headers[this.header] || request.headers[this.header.toLowerCase()]; if (!signature) return false; const bodyStr = typeof request.body === 'string' ? request.body : JSON.stringify(request.body); const expectedSignature = (0, crypto_1.createHmac)('sha256', this.secret) .update(bodyStr) .digest('hex'); return signature === expectedSignature; } } exports.SignatureAuthStrategy = SignatureAuthStrategy; const workflowRegistry = new Map(); function registerWebhookWorkflow(name, WorkflowClass) { workflowRegistry.set(name, WorkflowClass); } exports.registerWebhookWorkflow = registerWebhookWorkflow; class WebhookRouter { constructor(config = {}) { if (!config.authStrategy) { throw new Error('WebhookRouter requires an authentication strategy. Use NoneAuthStrategy explicitly if you want no authentication (not recommended for production).'); } this.authStrategy = config.authStrategy; this.registry = config.workflowRegistry || workflowRegistry; } async handle(request) { const isAuthenticated = await this.authStrategy.authenticate(request); if (!isAuthenticated) { return { statusCode: 401, body: { error: 'Unauthorized' }, }; } const pathParts = request.path.split('/').filter(p => p); if (request.method === 'POST' && pathParts[0] === 'start' && pathParts.length === 2) { return await this.handleStart(pathParts[1], request.body); } if (request.method === 'POST' && pathParts[0] === 'signal' && pathParts.length === 4) { return await this.handleSignal(pathParts[1], pathParts[2], pathParts[3], request.body); } return { statusCode: 404, body: { error: 'Not found' }, }; } async handleStart(workflowName, body) { try { const WorkflowClass = this.registry.get(workflowName); if (!WorkflowClass) { return { statusCode: 404, body: { error: `Workflow ${workflowName} not found` }, }; } // Check if the workflow start (execute method) is exposed via webhook const webhookMethods = (0, decorators_1.getWebhookMethods)(WorkflowClass); if (!webhookMethods.includes('execute')) { return { statusCode: 404, body: { error: 'Not found' }, }; } const payload = isRecord(body) ? body : {}; const args = Array.isArray(payload.args) ? payload.args : []; const id = typeof payload.id === 'string' ? payload.id : undefined; const handle = await WorkflowStub_1.WorkflowStub.make(WorkflowClass, id ? { id } : undefined); await handle.start(...args); return { statusCode: 200, body: { workflowId: handle.id, status: 'started', }, }; } catch (error) { const instance = global_1.Durabull.getActive(); const logger = (0, logger_1.createLoggerFromConfig)(instance?.getConfig().logger); const requestId = Math.random().toString(36).substring(7); logger.error(`Webhook start failed (req=${requestId})`, error); return { statusCode: 500, body: { error: 'Internal Server Error', requestId }, }; } } async handleSignal(workflowName, workflowId, signalName, body) { try { const WorkflowClass = this.registry.get(workflowName); if (!WorkflowClass) { return { statusCode: 404, body: { error: `Workflow ${workflowName} not found` }, }; } const signalMethods = (0, decorators_1.getSignalMethods)(WorkflowClass); if (!signalMethods.includes(signalName)) { return { statusCode: 404, body: { error: `Signal ${signalName} not found on workflow ${workflowName}` }, }; } const webhookMethods = (0, decorators_1.getWebhookMethods)(WorkflowClass); if (webhookMethods.length === 0 || !webhookMethods.includes(signalName)) { return { statusCode: 404, body: { error: 'Not found' }, }; } const rawPayload = isRecord(body) ? body.payload : undefined; const payload = Array.isArray(rawPayload) ? rawPayload : rawPayload !== undefined ? [rawPayload] : []; await WorkflowStub_1.WorkflowStub.sendSignal(workflowId, signalName, payload); return { statusCode: 200, body: { workflowId, signal: signalName, status: 'sent', }, }; } catch (error) { const instance = global_1.Durabull.getActive(); const logger = (0, logger_1.createLoggerFromConfig)(instance?.getConfig().logger); const requestId = Math.random().toString(36).substring(7); logger.error(`Webhook signal failed (req=${requestId})`, error); return { statusCode: 500, body: { error: 'Internal Server Error', requestId }, }; } } } exports.WebhookRouter = WebhookRouter; function createWebhookRouter(config) { return new WebhookRouter(config); } exports.createWebhookRouter = createWebhookRouter;