mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-03 15:43:11 +08:00
Refactor and optimize templates and code structure
- Deleted outdated templates for epics, product brief, and requirements PRD. - Introduced lazy loading for locale messages in i18n module to enhance performance. - Updated main application bootstrap to parallelize CSRF token fetching and locale loading. - Implemented code splitting for router configuration to optimize bundle size and loading times. - Added WebSocket connection limits and rate limiting to improve server performance and prevent abuse. - Enhanced input validation with compiled regex patterns for better performance and maintainability.
This commit is contained in:
@@ -30,6 +30,15 @@ import { deepMerge } from '../../types/util.js';
|
||||
|
||||
// ========== Input Validation ==========
|
||||
|
||||
// Compiled regex patterns for performance (compiled once at module load)
|
||||
const TELEGRAM_BOT_TOKEN_REGEX = /^\d{8,15}:[A-Za-z0-9_-]{30,50}$/;
|
||||
const TELEGRAM_CHAT_ID_REGEX = /^-?\d{1,20}$/;
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const DISCORD_HOSTNAMES = ['discord.com', 'discordapp.com'];
|
||||
const FEISHU_HOSTNAMES = ['feishu.cn', 'larksuite.com', 'lark.com'];
|
||||
const DINGTALK_HOSTNAMES = ['dingtalk.com', 'oapi.dingtalk.com'];
|
||||
const WECOM_HOSTNAMES = ['qyapi.weixin.qq.com', 'work.weixin.qq.com'];
|
||||
|
||||
/**
|
||||
* Validate URL format (must be http or https)
|
||||
*/
|
||||
@@ -51,7 +60,7 @@ function isValidDiscordWebhookUrl(url: string): boolean {
|
||||
const parsed = new URL(url);
|
||||
// Discord webhooks are typically: discord.com/api/webhooks/{id}/{token}
|
||||
return (
|
||||
(parsed.hostname === 'discord.com' || parsed.hostname === 'discordapp.com') &&
|
||||
DISCORD_HOSTNAMES.includes(parsed.hostname) &&
|
||||
parsed.pathname.startsWith('/api/webhooks/')
|
||||
);
|
||||
} catch {
|
||||
@@ -65,7 +74,7 @@ function isValidDiscordWebhookUrl(url: string): boolean {
|
||||
function isValidTelegramBotToken(token: string): boolean {
|
||||
// Telegram bot tokens are in format: {bot_id}:{token}
|
||||
// Bot ID is a number, token is alphanumeric with underscores and hyphens
|
||||
return /^\d{8,15}:[A-Za-z0-9_-]{30,50}$/.test(token);
|
||||
return TELEGRAM_BOT_TOKEN_REGEX.test(token);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +82,7 @@ function isValidTelegramBotToken(token: string): boolean {
|
||||
*/
|
||||
function isValidTelegramChatId(chatId: string): boolean {
|
||||
// Chat IDs are numeric, optionally negative (for groups)
|
||||
return /^-?\d{1,20}$/.test(chatId);
|
||||
return TELEGRAM_CHAT_ID_REGEX.test(chatId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +147,7 @@ function isValidDingTalkWebhookUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
// DingTalk webhooks are typically: oapi.dingtalk.com/robot/send?access_token=xxx
|
||||
return parsed.hostname.includes('dingtalk.com') && parsed.pathname.includes('robot');
|
||||
return DINGTALK_HOSTNAMES.some(h => parsed.hostname.includes(h)) && parsed.pathname.includes('robot');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -152,7 +161,7 @@ function isValidWeComWebhookUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
// WeCom webhooks are typically: qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
|
||||
return parsed.hostname.includes('qyapi.weixin.qq.com') && parsed.pathname.includes('webhook');
|
||||
return WECOM_HOSTNAMES.includes(parsed.hostname) && parsed.pathname.includes('webhook');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -162,8 +171,8 @@ function isValidWeComWebhookUrl(url: string): boolean {
|
||||
* Validate email address format
|
||||
*/
|
||||
function isValidEmail(email: string): boolean {
|
||||
// Basic email validation regex
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
// Basic email validation regex (using compiled constant)
|
||||
return EMAIL_REGEX.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,9 +13,55 @@ import type {
|
||||
QueueSchedulerConfigUpdatedMessage,
|
||||
} from '../types/queue-types.js';
|
||||
|
||||
// WebSocket configuration for connection limits and rate limiting
|
||||
const WS_MAX_CONNECTIONS = 100;
|
||||
const WS_MESSAGE_RATE_LIMIT = 10; // messages per second per client
|
||||
const WS_RATE_LIMIT_WINDOW = 1000; // 1 second window
|
||||
|
||||
// WebSocket clients for real-time notifications
|
||||
export const wsClients = new Set<Duplex>();
|
||||
|
||||
// Track message counts per client for rate limiting
|
||||
export const wsClientMessageCounts = new Map<Duplex, { count: number; resetTime: number }>();
|
||||
|
||||
/**
|
||||
* Check if a new WebSocket connection should be accepted
|
||||
* Returns true if connection allowed, false if limit reached
|
||||
*/
|
||||
export function canAcceptWebSocketConnection(): boolean {
|
||||
return wsClients.size < WS_MAX_CONNECTIONS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a client has exceeded their message rate limit
|
||||
* Returns true if rate limit OK, false if exceeded
|
||||
*/
|
||||
export function checkClientRateLimit(client: Duplex): boolean {
|
||||
const now = Date.now();
|
||||
const tracking = wsClientMessageCounts.get(client);
|
||||
|
||||
if (!tracking || now > tracking.resetTime) {
|
||||
// First message or window expired - reset tracking
|
||||
wsClientMessageCounts.set(client, { count: 1, resetTime: now + WS_RATE_LIMIT_WINDOW });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tracking.count >= WS_MESSAGE_RATE_LIMIT) {
|
||||
return false; // Rate limit exceeded
|
||||
}
|
||||
|
||||
tracking.count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up client tracking when they disconnect
|
||||
*/
|
||||
export function removeClientTracking(client: Duplex): void {
|
||||
wsClients.delete(client);
|
||||
wsClientMessageCounts.delete(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket message types for Loop monitoring
|
||||
*/
|
||||
@@ -154,6 +200,24 @@ export function handleWebSocketUpgrade(req: IncomingMessage, socket: Duplex, _he
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check connection limit
|
||||
if (!canAcceptWebSocketConnection()) {
|
||||
const responseHeaders = [
|
||||
'HTTP/1.1 429 Too Many Requests',
|
||||
'Content-Type: text/plain',
|
||||
'Connection: close',
|
||||
'',
|
||||
'WebSocket connection limit reached. Please try again later.',
|
||||
'',
|
||||
''
|
||||
].join('\r\n');
|
||||
socket.write(responseHeaders);
|
||||
socket.end();
|
||||
console.warn(`[WS] Connection rejected: limit reached (${wsClients.size}/${WS_MAX_CONNECTIONS})`);
|
||||
return;
|
||||
}
|
||||
|
||||
const acceptKey = createHash('sha1')
|
||||
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
||||
.digest('base64');
|
||||
@@ -171,6 +235,8 @@ export function handleWebSocketUpgrade(req: IncomingMessage, socket: Duplex, _he
|
||||
|
||||
// Add to clients set
|
||||
wsClients.add(socket);
|
||||
// Initialize rate limit tracking
|
||||
wsClientMessageCounts.set(socket, { count: 0, resetTime: Date.now() + WS_RATE_LIMIT_WINDOW });
|
||||
console.log(`[WS] Client connected (${wsClients.size} total)`);
|
||||
|
||||
// Replay any buffered A2UI surfaces to the new client
|
||||
@@ -194,6 +260,11 @@ export function handleWebSocketUpgrade(req: IncomingMessage, socket: Duplex, _he
|
||||
switch (opcode) {
|
||||
case 0x1: // Text frame
|
||||
if (payload) {
|
||||
// Check rate limit before processing
|
||||
if (!checkClientRateLimit(socket)) {
|
||||
console.warn('[WS] Rate limit exceeded for client');
|
||||
break;
|
||||
}
|
||||
console.log('[WS] Received:', payload);
|
||||
// Try to handle as A2UI message
|
||||
const handledAsA2UI = handleA2UIMessage(payload, a2uiWebSocketHandler, handleAnswer);
|
||||
@@ -227,12 +298,12 @@ export function handleWebSocketUpgrade(req: IncomingMessage, socket: Duplex, _he
|
||||
|
||||
// Handle disconnect
|
||||
socket.on('close', () => {
|
||||
wsClients.delete(socket);
|
||||
removeClientTracking(socket);
|
||||
console.log(`[WS] Client disconnected (${wsClients.size} remaining)`);
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
wsClients.delete(socket);
|
||||
removeClientTracking(socket);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user