durabull
Version:
A durable workflow engine built on top of BullMQ and Redis
179 lines (178 loc) • 7.31 kB
JavaScript
;
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;