claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
362 lines (361 loc) • 12.2 kB
JavaScript
/**
* Trigger.dev Webhook Handlers for CFN Loop Integration
*
* Express router providing secure webhook endpoints for:
* - Agent completion events
* - Gate check results
* - Consensus collection results
* - Product Owner decisions
*
* Configuration:
* - WEBHOOK_SECRET: Shared secret for HMAC signature verification
* - WEBHOOK_ALGORITHM: Hash algorithm (default: sha256)
*/ import { Router } from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
/**
* Webhook validation error class
*/ export class WebhookValidationError extends Error {
reason;
statusCode;
constructor(message, reason, statusCode = 401){
super(message), this.reason = reason, this.statusCode = statusCode;
this.name = 'WebhookValidationError';
}
}
/**
* TriggerDevWebhooks - Express router for secure webhook handling
*
* Provides endpoints:
* - POST /webhooks/agent-complete
* - POST /webhooks/gate-result
* - POST /webhooks/consensus-result
* - POST /webhooks/po-decision
*/ export class TriggerDevWebhooks {
router;
config;
verificationOptions;
// Handler maps for dependency injection
agentCompleteHandler;
gateResultHandler;
consensusResultHandler;
poDecisionHandler;
constructor(config){
this.router = Router();
this.config = {
secret: config?.secret || process.env.WEBHOOK_SECRET || 'default-secret',
algorithm: config?.algorithm || process.env.WEBHOOK_ALGORITHM || 'sha256',
verifySignature: config?.verifySignature !== false
};
this.verificationOptions = {
algorithmType: this.config.algorithm || 'sha256',
headerName: 'x-trigger-signature'
};
this.setupRoutes();
}
/**
* Register handler for agent completion events
*/ onAgentComplete(handler) {
this.agentCompleteHandler = handler;
return this;
}
/**
* Register handler for gate result events
*/ onGateResult(handler) {
this.gateResultHandler = handler;
return this;
}
/**
* Register handler for consensus result events
*/ onConsensusResult(handler) {
this.consensusResultHandler = handler;
return this;
}
/**
* Register handler for Product Owner decision events
*/ onPODecision(handler) {
this.poDecisionHandler = handler;
return this;
}
/**
* Get Express router for mounting
*/ getRouter() {
return this.router;
}
/**
* Setup all webhook routes
*
* @private
*/ setupRoutes() {
// Middleware for signature verification
this.router.use(this.signatureVerificationMiddleware.bind(this));
// Agent completion endpoint
this.router.post('/agent-complete', this.handleAgentComplete.bind(this));
// Gate result endpoint
this.router.post('/gate-result', this.handleGateResult.bind(this));
// Consensus result endpoint
this.router.post('/consensus-result', this.handleConsensusResult.bind(this));
// PO decision endpoint
this.router.post('/po-decision', this.handlePODecision.bind(this));
// Error handling middleware
this.router.use(this.errorHandler.bind(this));
}
/**
* Signature verification middleware
*
* TODO: RUNTIME_TEST: Verify HMAC signature validation with valid/invalid keys
*
* @private
*/ async signatureVerificationMiddleware(req, res, next) {
try {
if (!this.config.verifySignature) {
return next();
}
const signature = req.headers[this.verificationOptions.headerName];
if (!signature) {
throw new WebhookValidationError('Missing webhook signature', 'MISSING_SIGNATURE');
}
const body = JSON.stringify(req.body);
const isValid = await this.verifySignature(body, signature);
if (!isValid) {
throw new WebhookValidationError('Invalid webhook signature', 'INVALID_SIGNATURE');
}
next();
} catch (error) {
if (error instanceof WebhookValidationError) {
return res.status(error.statusCode).json({
success: false,
message: error.message,
reason: error.reason
});
}
return res.status(500).json({
success: false,
message: 'Signature verification failed',
reason: 'INTERNAL_ERROR'
});
}
}
/**
* Handle agent completion webhook
*
* @private
*/ async handleAgentComplete(req, res) {
try {
const payload = req.body;
// Validate required fields
if (!payload.agentId || !payload.taskId) {
return res.status(400).json({
success: false,
message: 'Missing required fields',
data: null
});
}
const context = {
payload,
isVerified: this.config.verifySignature,
timestamp: Date.now(),
signature: req.headers[this.verificationOptions.headerName]
};
if (this.agentCompleteHandler) {
const result = await this.agentCompleteHandler(context);
return res.status(200).json(result);
}
res.status(200).json({
success: true,
message: 'Agent completion event received',
data: {
agentId: payload.agentId
}
});
} catch (error) {
return res.status(500).json({
success: false,
message: 'Failed to handle agent completion',
data: null
});
}
}
/**
* Handle gate result webhook
*
* @private
*/ async handleGateResult(req, res) {
try {
const payload = req.body;
if (!payload.taskId || payload.gateType === undefined) {
return res.status(400).json({
success: false,
message: 'Missing required fields',
data: null
});
}
const context = {
payload,
isVerified: this.config.verifySignature,
timestamp: Date.now(),
signature: req.headers[this.verificationOptions.headerName]
};
if (this.gateResultHandler) {
const result = await this.gateResultHandler(context);
return res.status(200).json(result);
}
res.status(200).json({
success: true,
message: 'Gate result received',
data: {
taskId: payload.taskId,
passed: payload.passed
}
});
} catch (error) {
return res.status(500).json({
success: false,
message: 'Failed to handle gate result',
data: null
});
}
}
/**
* Handle consensus result webhook
*
* @private
*/ async handleConsensusResult(req, res) {
try {
const payload = req.body;
if (!payload.taskId || payload.validatorCount === undefined) {
return res.status(400).json({
success: false,
message: 'Missing required fields',
data: null
});
}
const context = {
payload,
isVerified: this.config.verifySignature,
timestamp: Date.now(),
signature: req.headers[this.verificationOptions.headerName]
};
if (this.consensusResultHandler) {
const result = await this.consensusResultHandler(context);
return res.status(200).json(result);
}
res.status(200).json({
success: true,
message: 'Consensus result received',
data: {
taskId: payload.taskId,
consensusScore: payload.consensusScore
}
});
} catch (error) {
return res.status(500).json({
success: false,
message: 'Failed to handle consensus result',
data: null
});
}
}
/**
* Handle Product Owner decision webhook
*
* @private
*/ async handlePODecision(req, res) {
try {
const payload = req.body;
if (!payload.taskId || !payload.decision) {
return res.status(400).json({
success: false,
message: 'Missing required fields',
data: null
});
}
// Validate decision enum
if (![
'PROCEED',
'ITERATE',
'ABORT'
].includes(payload.decision)) {
return res.status(400).json({
success: false,
message: 'Invalid decision value',
data: null
});
}
const context = {
payload,
isVerified: this.config.verifySignature,
timestamp: Date.now(),
signature: req.headers[this.verificationOptions.headerName]
};
if (this.poDecisionHandler) {
const result = await this.poDecisionHandler(context);
return res.status(200).json(result);
}
res.status(200).json({
success: true,
message: 'PO decision received',
data: {
taskId: payload.taskId,
decision: payload.decision
}
});
} catch (error) {
return res.status(500).json({
success: false,
message: 'Failed to handle PO decision',
data: null
});
}
}
/**
* Verify HMAC signature of webhook payload
*
* @private
* @param payload Raw request body as string
* @param signature Signature header value
* @returns Promise resolving to true if signature is valid
*/ async verifySignature(payload, signature) {
try {
const hmac = createHmac(this.verificationOptions.algorithmType, this.config.secret);
hmac.update(payload, 'utf8');
const expectedSignature = hmac.digest('hex');
// Use timing-safe comparison
return timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
} catch (error) {
console.error('Signature verification error:', error);
return false;
}
}
/**
* Error handling middleware
*
* @private
*/ errorHandler(error, req, res, next) {
console.error('Webhook error:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
data: null
});
}
}
/**
* Create webhook router with optional handlers
*/ export const createWebhookRouter = (config, handlers)=>{
const webhooks = new TriggerDevWebhooks(config);
if (handlers?.onAgentComplete) {
webhooks.onAgentComplete(handlers.onAgentComplete);
}
if (handlers?.onGateResult) {
webhooks.onGateResult(handlers.onGateResult);
}
if (handlers?.onConsensusResult) {
webhooks.onConsensusResult(handlers.onConsensusResult);
}
if (handlers?.onPODecision) {
webhooks.onPODecision(handlers.onPODecision);
}
return webhooks.getRouter();
};
export default TriggerDevWebhooks;
//# sourceMappingURL=trigger-dev-webhooks.js.map