@openclueo/express-clueobots
Version:
Express.js middleware for ClueoBots AI Security - One line of code, full protection
226 lines (225 loc) • 9.05 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.clueobots = clueobots;
const clueobots_1 = __importDefault(require("@openclueo/clueobots"));
/**
* ClueoBots Express.js middleware for AI security protection
*
* @example
* ```typescript
* import express from 'express'
* import { clueobots } from '@openclueo/express-clueobots'
*
* const app = express()
*
* // Protect all routes with one line
* app.use(clueobots({ apiKey: 'your-api-key' }))
*
* // Your routes are now automatically protected
* app.post('/api/chat', (req, res) => {
* // req.body is already scanned for threats
* res.json({ message: 'Safe to proceed!' })
* })
* ```
*/
function clueobots(options = {}) {
// Initialize ClueoBots client
const client = new clueobots_1.default({
apiKey: options.apiKey || process.env.CLUEOBOTS_API_KEY || '',
baseUrl: options.baseUrl
});
// Default configuration
const config = {
blockThreshold: options.blockThreshold || 'medium',
blockRequests: options.blockRequests !== false,
debug: options.debug || false,
skipContentTypes: options.skipContentTypes || ['image/*', 'video/*', 'audio/*'],
maxBodySize: options.maxBodySize || 1048576, // 1MB
...options
};
// Threat level to numeric score mapping
const threatLevels = { low: 1, medium: 2, high: 3, critical: 4 };
// Path matching helper
const shouldScanPath = (path) => {
// Check exclude paths first
if (config.excludePaths?.some(pattern => matchPath(path, pattern))) {
return false;
}
// If specific paths are defined, only scan those
if (config.paths?.length) {
return config.paths.some(pattern => matchPath(path, pattern));
}
// Default: scan all paths
return true;
};
// Simple path pattern matching
const matchPath = (path, pattern) => {
if (pattern.includes('*')) {
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
return regex.test(path);
}
return path.startsWith(pattern);
};
// Content extraction helper
const extractContentFromRequest = (req) => {
const content = [];
// Extract from request body
if (req.body) {
if (typeof req.body === 'string') {
content.push(req.body);
}
else if (typeof req.body === 'object') {
// Look for common fields that might contain user input
const textFields = ['message', 'prompt', 'content', 'text', 'input', 'query', 'description'];
textFields.forEach(field => {
if (req.body[field] && typeof req.body[field] === 'string') {
content.push(req.body[field]);
}
});
// Recursively extract text from nested objects
content.push(...extractTextFromObject(req.body));
}
}
// Extract from query parameters
Object.values(req.query).forEach(value => {
if (typeof value === 'string') {
content.push(value);
}
});
return content.filter(text => text && text.length > 0);
};
// Helper to recursively extract text from objects
const extractTextFromObject = (obj, maxDepth = 3) => {
if (maxDepth <= 0)
return [];
const texts = [];
for (const value of Object.values(obj)) {
if (typeof value === 'string') {
texts.push(value);
}
else if (typeof value === 'object' && value !== null) {
texts.push(...extractTextFromObject(value, maxDepth - 1));
}
}
return texts;
};
// Log helper
const log = (...args) => {
if (config.debug) {
console.log('[ClueoBots]', ...args);
}
};
// Main middleware function
return async (req, res, next) => {
const startTime = Date.now();
try {
// Skip if path doesn't match
if (!shouldScanPath(req.path)) {
log(`Skipping scan for path: ${req.path}`);
return next();
}
// Skip if content type should be skipped
const contentType = req.get('content-type') || '';
if (config.skipContentTypes.some(skip => skip.includes('*') ? contentType.startsWith(skip.replace('*', '')) : contentType === skip)) {
log(`Skipping scan for content type: ${contentType}`);
return next();
}
// Skip if body is too large
const contentLength = parseInt(req.get('content-length') || '0');
if (contentLength > config.maxBodySize) {
log(`Skipping scan - body too large: ${contentLength} bytes`);
return next();
}
// Extract content to scan
const contentToScan = extractContentFromRequest(req);
if (contentToScan.length === 0) {
log(`No content to scan for ${req.method} ${req.path}`);
return next();
}
log(`Scanning ${contentToScan.length} pieces of content for ${req.method} ${req.path}`);
// Scan all content pieces
const scanPromises = contentToScan.map(content => client.scanContent({
content,
type: 'text',
context: `${req.method} ${req.path}`
}));
const scanResults = await Promise.all(scanPromises);
// Find the highest threat level
let highestThreat = null;
for (const result of scanResults) {
if (result.threat_detected) {
const threat = {
detected: result.threat_detected,
threatType: result.threat_type,
riskLevel: result.risk_level,
confidence: result.confidence,
explanation: result.explanation,
suggestions: result.suggestions,
scanId: result.scan_id,
timestamp: result.timestamp
};
if (!highestThreat || threatLevels[threat.riskLevel] > threatLevels[highestThreat.riskLevel]) {
highestThreat = threat;
}
}
}
// Add scan info to request
req.clueobots = {
scanned: true,
threat: highestThreat || undefined,
scanTime: Date.now() - startTime
};
// Handle threat detection
if (highestThreat) {
log(`🚨 Threat detected: ${highestThreat.threatType} (${highestThreat.riskLevel})`);
// Call custom threat handler if provided
if (config.onThreat) {
config.onThreat(highestThreat, req, res);
return;
}
// Check if we should block based on threshold
const shouldBlock = config.blockRequests &&
threatLevels[highestThreat.riskLevel] >= threatLevels[config.blockThreshold];
if (shouldBlock) {
log(`🛡️ Blocking request due to ${highestThreat.riskLevel} threat`);
return res.status(400).json({
error: 'Security threat detected',
details: {
type: highestThreat.threatType,
riskLevel: highestThreat.riskLevel,
explanation: highestThreat.explanation,
suggestions: highestThreat.suggestions,
scanId: highestThreat.scanId
},
timestamp: new Date().toISOString()
});
}
}
else {
log(`✅ No threats detected for ${req.method} ${req.path}`);
}
next();
}
catch (error) {
const scanError = error;
log(`❌ Scan error: ${scanError.message}`);
// Call custom error handler if provided
if (config.onError) {
config.onError(scanError, req, res);
return;
}
// In case of error, log but don't block the request (fail open)
console.error('[ClueoBots] Scan error:', scanError);
req.clueobots = {
scanned: false,
scanTime: Date.now() - startTime
};
next();
}
};
}
// Default export
exports.default = clueobots;