From bf0df9b6d056ce672651d912ce3d080cee7156ee Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Fri, 27 Feb 2026 13:24:36 +0700 Subject: [PATCH] fix: strict mode, test stubs, pagination counts, N+1 queries, CSP hardening - Enable TypeScript strict mode and fix all resulting type errors - Add auth test stubs for requireRole and safeCompare - Add proper COUNT(*) pagination totals to agents, tasks, notifications, messages, conversations, and standup history endpoints - Fix N+1 queries by hoisting db.prepare() outside loops in agents, activities, notifications, conversations, standup, gateway health, and notification delivery routes - Remove unsafe-eval from CSP script-src directive - Remove deprecated X-XSS-Protection header --- next.config.js | 3 +- src/app/api/activities/route.ts | 42 +++--- src/app/api/agents/[id]/heartbeat/route.ts | 2 +- src/app/api/agents/[id]/memory/route.ts | 6 +- src/app/api/agents/[id]/soul/route.ts | 4 +- src/app/api/agents/route.ts | 45 ++++-- src/app/api/chat/conversations/route.ts | 33 +++-- src/app/api/chat/messages/route.ts | 23 +++- src/app/api/gateways/health/route.ts | 16 ++- src/app/api/notifications/deliver/route.ts | 8 +- src/app/api/notifications/route.ts | 58 +++++--- src/app/api/standup/route.ts | 130 +++++++++--------- src/app/api/tasks/route.ts | 19 ++- .../panels/memory-browser-panel.tsx | 2 +- .../panels/token-dashboard-panel.tsx | 6 +- src/lib/__tests__/auth.test.ts | 17 +++ tsconfig.json | 2 +- 17 files changed, 266 insertions(+), 150 deletions(-) create mode 100644 src/lib/__tests__/auth.test.ts diff --git a/next.config.js b/next.config.js index baddbeb..d3f7174 100644 --- a/next.config.js +++ b/next.config.js @@ -8,7 +8,7 @@ const nextConfig = { const csp = [ `default-src 'self'`, - `script-src 'self' 'unsafe-inline' 'unsafe-eval'${googleEnabled ? ' https://accounts.google.com' : ''}`, + `script-src 'self' 'unsafe-inline'${googleEnabled ? ' https://accounts.google.com' : ''}`, `style-src 'self' 'unsafe-inline'`, `connect-src 'self' ws: wss: http://127.0.0.1:* http://localhost:*`, `img-src 'self' data: blob:${googleEnabled ? ' https://*.googleusercontent.com https://lh3.googleusercontent.com' : ''}`, @@ -22,7 +22,6 @@ const nextConfig = { headers: [ { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, - { key: 'X-XSS-Protection', value: '1; mode=block' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Content-Security-Policy', value: csp }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, diff --git a/src/app/api/activities/route.ts b/src/app/api/activities/route.ts index f16b25d..71dad92 100644 --- a/src/app/api/activities/route.ts +++ b/src/app/api/activities/route.ts @@ -72,48 +72,52 @@ async function handleActivitiesRequest(request: NextRequest) { const stmt = db.prepare(query); const activities = stmt.all(...params) as Activity[]; + // Prepare entity detail statements once (avoids N+1) + const taskDetailStmt = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?'); + const agentDetailStmt = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?'); + const commentDetailStmt = db.prepare(` + SELECT c.id, c.content, c.task_id, t.title as task_title + FROM comments c + LEFT JOIN tasks t ON c.task_id = t.id + WHERE c.id = ? + `); + // Parse JSON data field and enhance with related entity data const enhancedActivities = activities.map(activity => { let entityDetails = null; - + try { - // Fetch related entity details based on entity_type switch (activity.entity_type) { - case 'task': - const task = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?').get(activity.entity_id) as any; + case 'task': { + const task = taskDetailStmt.get(activity.entity_id) as any; if (task) { entityDetails = { type: 'task', ...task }; } break; - - case 'agent': - const agent = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?').get(activity.entity_id) as any; + } + case 'agent': { + const agent = agentDetailStmt.get(activity.entity_id) as any; if (agent) { entityDetails = { type: 'agent', ...agent }; } break; - - case 'comment': - const comment = db.prepare(` - SELECT c.id, c.content, c.task_id, t.title as task_title - FROM comments c - LEFT JOIN tasks t ON c.task_id = t.id - WHERE c.id = ? - `).get(activity.entity_id) as any; + } + case 'comment': { + const comment = commentDetailStmt.get(activity.entity_id) as any; if (comment) { - entityDetails = { - type: 'comment', + entityDetails = { + type: 'comment', ...comment, content_preview: comment.content?.substring(0, 100) || '' }; } break; + } } } catch (error) { - // If entity lookup fails, continue without entity details console.warn(`Failed to fetch entity details for activity ${activity.id}:`, error); } - + return { ...activity, data: activity.data ? JSON.parse(activity.data) : null, diff --git a/src/app/api/agents/[id]/heartbeat/route.ts b/src/app/api/agents/[id]/heartbeat/route.ts index 4d40b35..d5f758e 100644 --- a/src/app/api/agents/[id]/heartbeat/route.ts +++ b/src/app/api/agents/[id]/heartbeat/route.ts @@ -25,7 +25,7 @@ export async function GET( const agentId = resolvedParams.id; // Get agent by ID or name - let agent; + let agent: any; if (isNaN(Number(agentId))) { // Lookup by name agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); diff --git a/src/app/api/agents/[id]/memory/route.ts b/src/app/api/agents/[id]/memory/route.ts index 1047e74..319c1b0 100644 --- a/src/app/api/agents/[id]/memory/route.ts +++ b/src/app/api/agents/[id]/memory/route.ts @@ -21,7 +21,7 @@ export async function GET( const agentId = resolvedParams.id; // Get agent by ID or name - let agent; + let agent: any; if (isNaN(Number(agentId))) { agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); } else { @@ -81,7 +81,7 @@ export async function PUT( const { working_memory, append } = body; // Get agent by ID or name - let agent; + let agent: any; if (isNaN(Number(agentId))) { agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); } else { @@ -168,7 +168,7 @@ export async function DELETE( const agentId = resolvedParams.id; // Get agent by ID or name - let agent; + let agent: any; if (isNaN(Number(agentId))) { agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); } else { diff --git a/src/app/api/agents/[id]/soul/route.ts b/src/app/api/agents/[id]/soul/route.ts index df097ad..07d24d3 100644 --- a/src/app/api/agents/[id]/soul/route.ts +++ b/src/app/api/agents/[id]/soul/route.ts @@ -22,7 +22,7 @@ export async function GET( const agentId = resolvedParams.id; // Get agent by ID or name - let agent; + let agent: any; if (isNaN(Number(agentId))) { agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); } else { @@ -81,7 +81,7 @@ export async function PUT( const { soul_content, template_name } = body; // Get agent by ID or name - let agent; + let agent: any; if (isNaN(Number(agentId))) { agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId); } else { diff --git a/src/app/api/agents/route.ts b/src/app/api/agents/route.ts index f21017c..a51cf1a 100644 --- a/src/app/api/agents/route.ts +++ b/src/app/api/agents/route.ts @@ -50,20 +50,20 @@ export async function GET(request: NextRequest) { config: agent.config ? JSON.parse(agent.config) : {} })); - // Get task counts for each agent + // Get task counts for each agent (prepare once, reuse per agent) + const taskCountStmt = db.prepare(` + SELECT + COUNT(*) as total, + SUM(CASE WHEN status = 'assigned' THEN 1 ELSE 0 END) as assigned, + SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress, + SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as completed + FROM tasks + WHERE assigned_to = ? + `); + const agentsWithStats = agentsWithParsedData.map(agent => { - const taskCountStmt = db.prepare(` - SELECT - COUNT(*) as total, - SUM(CASE WHEN status = 'assigned' THEN 1 ELSE 0 END) as assigned, - SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress, - SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as completed - FROM tasks - WHERE assigned_to = ? - `); - const taskStats = taskCountStmt.get(agent.name) as any; - + return { ...agent, taskStats: { @@ -75,9 +75,24 @@ export async function GET(request: NextRequest) { }; }); - return NextResponse.json({ - agents: agentsWithStats, - total: agents.length + // Get total count for pagination + let countQuery = 'SELECT COUNT(*) as total FROM agents WHERE 1=1'; + const countParams: any[] = []; + if (status) { + countQuery += ' AND status = ?'; + countParams.push(status); + } + if (role) { + countQuery += ' AND role = ?'; + countParams.push(role); + } + const countRow = db.prepare(countQuery).get(...countParams) as { total: number }; + + return NextResponse.json({ + agents: agentsWithStats, + total: countRow.total, + page: Math.floor(offset / limit) + 1, + limit }); } catch (error) { console.error('GET /api/agents error:', error); diff --git a/src/app/api/chat/conversations/route.ts b/src/app/api/chat/conversations/route.ts index b2b7d7c..f2d16d2 100644 --- a/src/app/api/chat/conversations/route.ts +++ b/src/app/api/chat/conversations/route.ts @@ -55,14 +55,16 @@ export async function GET(request: NextRequest) { const conversations = db.prepare(query).all(...params) as any[] - // Fetch the last message for each conversation + // Prepare last message statement once (avoids N+1) + const lastMsgStmt = db.prepare(` + SELECT * FROM messages + WHERE conversation_id = ? + ORDER BY created_at DESC + LIMIT 1 + `); + const withLastMessage = conversations.map((conv) => { - const lastMsg = db.prepare(` - SELECT * FROM messages - WHERE conversation_id = ? - ORDER BY created_at DESC - LIMIT 1 - `).get(conv.conversation_id) as any + const lastMsg = lastMsgStmt.get(conv.conversation_id) as any; return { ...conv, @@ -75,7 +77,22 @@ export async function GET(request: NextRequest) { } }) - return NextResponse.json({ conversations: withLastMessage, total: withLastMessage.length }) + // Get total count for pagination + let countQuery: string + const countParams: any[] = [] + if (agent) { + countQuery = ` + SELECT COUNT(DISTINCT m.conversation_id) as total + FROM messages m + WHERE m.from_agent = ? OR m.to_agent = ? OR m.to_agent IS NULL + ` + countParams.push(agent, agent) + } else { + countQuery = 'SELECT COUNT(DISTINCT conversation_id) as total FROM messages' + } + const countRow = db.prepare(countQuery).get(...countParams) as { total: number } + + return NextResponse.json({ conversations: withLastMessage, total: countRow.total, page: Math.floor(offset / limit) + 1, limit }) } catch (error) { console.error('GET /api/chat/conversations error:', error) return NextResponse.json({ error: 'Failed to fetch conversations' }, { status: 500 }) diff --git a/src/app/api/chat/messages/route.ts b/src/app/api/chat/messages/route.ts index ae9d936..e062bcd 100644 --- a/src/app/api/chat/messages/route.ts +++ b/src/app/api/chat/messages/route.ts @@ -143,7 +143,28 @@ export async function GET(request: NextRequest) { metadata: msg.metadata ? JSON.parse(msg.metadata) : null })) - return NextResponse.json({ messages: parsed, total: parsed.length }) + // Get total count for pagination + let countQuery = 'SELECT COUNT(*) as total FROM messages WHERE 1=1' + const countParams: any[] = [] + if (conversation_id) { + countQuery += ' AND conversation_id = ?' + countParams.push(conversation_id) + } + if (from_agent) { + countQuery += ' AND from_agent = ?' + countParams.push(from_agent) + } + if (to_agent) { + countQuery += ' AND to_agent = ?' + countParams.push(to_agent) + } + if (since) { + countQuery += ' AND created_at > ?' + countParams.push(parseInt(since)) + } + const countRow = db.prepare(countQuery).get(...countParams) as { total: number } + + return NextResponse.json({ messages: parsed, total: countRow.total, page: Math.floor(offset / limit) + 1, limit }) } catch (error) { console.error('GET /api/chat/messages error:', error) return NextResponse.json({ error: 'Failed to fetch messages' }, { status: 500 }) diff --git a/src/app/api/gateways/health/route.ts b/src/app/api/gateways/health/route.ts index fe3c01c..6297183 100644 --- a/src/app/api/gateways/health/route.ts +++ b/src/app/api/gateways/health/route.ts @@ -47,6 +47,14 @@ export async function POST(request: NextRequest) { const db = getDatabase() const gateways = db.prepare("SELECT * FROM gateways ORDER BY is_primary DESC, name ASC").all() as GatewayEntry[] + // Prepare update statements once (avoids N+1) + const updateOnlineStmt = db.prepare( + "UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?" + ) + const updateOfflineStmt = db.prepare( + "UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?" + ) + const results: HealthResult[] = [] for (const gw of gateways) { @@ -70,9 +78,7 @@ export async function POST(request: NextRequest) { const latency = Date.now() - start const status = res.ok ? "online" : "error" - db.prepare( - "UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?" - ).run(status, latency, gw.id) + updateOnlineStmt.run(status, latency, gw.id) results.push({ id: gw.id, @@ -83,9 +89,7 @@ export async function POST(request: NextRequest) { sessions_count: 0, }) } catch (err: any) { - db.prepare( - "UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?" - ).run("offline", gw.id) + updateOfflineStmt.run("offline", gw.id) results.push({ id: gw.id, diff --git a/src/app/api/notifications/deliver/route.ts b/src/app/api/notifications/deliver/route.ts index 8281b90..e0018cd 100644 --- a/src/app/api/notifications/deliver/route.ts +++ b/src/app/api/notifications/deliver/route.ts @@ -56,7 +56,10 @@ export async function POST(request: NextRequest) { let errorCount = 0; const errors: any[] = []; const deliveryResults: any[] = []; - + + // Prepare update statement once (avoids N+1) + const markDeliveredStmt = db.prepare('UPDATE notifications SET delivered_at = ? WHERE id = ?'); + for (const notification of undeliveredNotifications) { try { // Skip if agent doesn't have session key @@ -94,8 +97,7 @@ export async function POST(request: NextRequest) { // Mark as delivered const now = Math.floor(Date.now() / 1000); - db.prepare('UPDATE notifications SET delivered_at = ? WHERE id = ?') - .run(now, notification.id); + markDeliveredStmt.run(now, notification.id); deliveredCount++; deliveryResults.push({ diff --git a/src/app/api/notifications/route.ts b/src/app/api/notifications/route.ts index 35b3f40..b12c99c 100644 --- a/src/app/api/notifications/route.ts +++ b/src/app/api/notifications/route.ts @@ -44,48 +44,54 @@ export async function GET(request: NextRequest) { const stmt = db.prepare(query); const notifications = stmt.all(...params) as Notification[]; + // Prepare source detail statements once (avoids N+1) + const taskDetailStmt = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?'); + const commentDetailStmt = db.prepare(` + SELECT c.id, c.content, c.task_id, t.title as task_title + FROM comments c + LEFT JOIN tasks t ON c.task_id = t.id + WHERE c.id = ? + `); + const agentDetailStmt = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?'); + // Enhance notifications with related entity data const enhancedNotifications = notifications.map(notification => { let sourceDetails = null; - + try { if (notification.source_type && notification.source_id) { switch (notification.source_type) { - case 'task': - const task = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?').get(notification.source_id) as any; + case 'task': { + const task = taskDetailStmt.get(notification.source_id) as any; if (task) { sourceDetails = { type: 'task', ...task }; } break; - - case 'comment': - const comment = db.prepare(` - SELECT c.id, c.content, c.task_id, t.title as task_title - FROM comments c - LEFT JOIN tasks t ON c.task_id = t.id - WHERE c.id = ? - `).get(notification.source_id) as any; + } + case 'comment': { + const comment = commentDetailStmt.get(notification.source_id) as any; if (comment) { - sourceDetails = { - type: 'comment', + sourceDetails = { + type: 'comment', ...comment, content_preview: comment.content?.substring(0, 100) || '' }; } break; - - case 'agent': - const agent = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?').get(notification.source_id) as any; + } + case 'agent': { + const agent = agentDetailStmt.get(notification.source_id) as any; if (agent) { sourceDetails = { type: 'agent', ...agent }; } break; + } } } } catch (error) { console.warn(`Failed to fetch source details for notification ${notification.id}:`, error); } - + return { ...notification, source: sourceDetails @@ -99,9 +105,23 @@ export async function GET(request: NextRequest) { WHERE recipient = ? AND read_at IS NULL `).get(recipient) as { count: number }; - return NextResponse.json({ + // Get total count for pagination + let countQuery = 'SELECT COUNT(*) as total FROM notifications WHERE recipient = ?'; + const countParams: any[] = [recipient]; + if (unread_only) { + countQuery += ' AND read_at IS NULL'; + } + if (type) { + countQuery += ' AND type = ?'; + countParams.push(type); + } + const countRow = db.prepare(countQuery).get(...countParams) as { total: number }; + + return NextResponse.json({ notifications: enhancedNotifications, - total: notifications.length, + total: countRow.total, + page: Math.floor(offset / limit) + 1, + limit, unreadCount: unreadCount.count }); } catch (error) { diff --git a/src/app/api/standup/route.ts b/src/app/api/standup/route.ts index a96bec9..3b791f6 100644 --- a/src/app/api/standup/route.ts +++ b/src/app/api/standup/route.ts @@ -36,71 +36,67 @@ export async function POST(request: NextRequest) { const agents = db.prepare(agentQuery).all(...agentParams) as any[]; + // Prepare statements once (avoids N+1 per agent) + const completedTasksStmt = db.prepare(` + SELECT id, title, status, updated_at + FROM tasks + WHERE assigned_to = ? + AND status = 'done' + AND updated_at BETWEEN ? AND ? + ORDER BY updated_at DESC + `); + const inProgressTasksStmt = db.prepare(` + SELECT id, title, status, created_at, due_date + FROM tasks + WHERE assigned_to = ? + AND status = 'in_progress' + ORDER BY created_at ASC + `); + const assignedTasksStmt = db.prepare(` + SELECT id, title, status, created_at, due_date, priority + FROM tasks + WHERE assigned_to = ? + AND status = 'assigned' + ORDER BY priority DESC, created_at ASC + `); + const reviewTasksStmt = db.prepare(` + SELECT id, title, status, updated_at + FROM tasks + WHERE assigned_to = ? + AND status IN ('review', 'quality_review') + ORDER BY updated_at ASC + `); + const blockedTasksStmt = db.prepare(` + SELECT id, title, status, priority, created_at, metadata + FROM tasks + WHERE assigned_to = ? + AND (priority = 'urgent' OR metadata LIKE '%blocked%') + AND status NOT IN ('done') + ORDER BY priority DESC, created_at ASC + `); + const activityCountStmt = db.prepare(` + SELECT COUNT(*) as count + FROM activities + WHERE actor = ? + AND created_at BETWEEN ? AND ? + `); + const commentCountStmt = db.prepare(` + SELECT COUNT(*) as count + FROM comments + WHERE author = ? + AND created_at BETWEEN ? AND ? + `); + // Generate standup data for each agent const standupData = agents.map(agent => { - // Completed tasks today - const completedTasks = db.prepare(` - SELECT id, title, status, updated_at - FROM tasks - WHERE assigned_to = ? - AND status = 'done' - AND updated_at BETWEEN ? AND ? - ORDER BY updated_at DESC - `).all(agent.name, startOfDay, endOfDay); - - // Currently in progress tasks - const inProgressTasks = db.prepare(` - SELECT id, title, status, created_at, due_date - FROM tasks - WHERE assigned_to = ? - AND status = 'in_progress' - ORDER BY created_at ASC - `).all(agent.name); - - // Assigned but not started tasks - const assignedTasks = db.prepare(` - SELECT id, title, status, created_at, due_date, priority - FROM tasks - WHERE assigned_to = ? - AND status = 'assigned' - ORDER BY priority DESC, created_at ASC - `).all(agent.name); - - // Review tasks - const reviewTasks = db.prepare(` - SELECT id, title, status, updated_at - FROM tasks - WHERE assigned_to = ? - AND status IN ('review', 'quality_review') - ORDER BY updated_at ASC - `).all(agent.name); - - // Blocked/high priority tasks - const blockedTasks = db.prepare(` - SELECT id, title, status, priority, created_at, metadata - FROM tasks - WHERE assigned_to = ? - AND (priority = 'urgent' OR metadata LIKE '%blocked%') - AND status NOT IN ('done') - ORDER BY priority DESC, created_at ASC - `).all(agent.name); - - // Recent activity count - const activityCount = db.prepare(` - SELECT COUNT(*) as count - FROM activities - WHERE actor = ? - AND created_at BETWEEN ? AND ? - `).get(agent.name, startOfDay, endOfDay) as { count: number }; - - // Comments made today - const commentsToday = db.prepare(` - SELECT COUNT(*) as count - FROM comments - WHERE author = ? - AND created_at BETWEEN ? AND ? - `).get(agent.name, startOfDay, endOfDay) as { count: number }; - + const completedTasks = completedTasksStmt.all(agent.name, startOfDay, endOfDay); + const inProgressTasks = inProgressTasksStmt.all(agent.name); + const assignedTasks = assignedTasksStmt.all(agent.name); + const reviewTasks = reviewTasksStmt.all(agent.name); + const blockedTasks = blockedTasksStmt.all(agent.name); + const activityCount = activityCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number }; + const commentsToday = commentCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number }; + return { agent: { name: agent.name, @@ -239,9 +235,13 @@ export async function GET(request: NextRequest) { }; }); - return NextResponse.json({ + const countRow = db.prepare('SELECT COUNT(*) as total FROM standup_reports').get() as { total: number }; + + return NextResponse.json({ history: standupHistory, - total: standupHistory.length + total: countRow.total, + page: Math.floor(offset / limit) + 1, + limit }); } catch (error) { console.error('GET /api/standup/history error:', error); diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 9b5a43c..3561273 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -64,7 +64,24 @@ export async function GET(request: NextRequest) { metadata: task.metadata ? JSON.parse(task.metadata) : {} })); - return NextResponse.json({ tasks: tasksWithParsedData, total: tasks.length }); + // Get total count for pagination + let countQuery = 'SELECT COUNT(*) as total FROM tasks WHERE 1=1'; + const countParams: any[] = []; + if (status) { + countQuery += ' AND status = ?'; + countParams.push(status); + } + if (assigned_to) { + countQuery += ' AND assigned_to = ?'; + countParams.push(assigned_to); + } + if (priority) { + countQuery += ' AND priority = ?'; + countParams.push(priority); + } + const countRow = db.prepare(countQuery).get(...countParams) as { total: number }; + + return NextResponse.json({ tasks: tasksWithParsedData, total: countRow.total, page: Math.floor(offset / limit) + 1, limit }); } catch (error) { console.error('GET /api/tasks error:', error); return NextResponse.json({ error: 'Failed to fetch tasks' }, { status: 500 }); diff --git a/src/components/panels/memory-browser-panel.tsx b/src/components/panels/memory-browser-panel.tsx index e40fdb7..5722b81 100644 --- a/src/components/panels/memory-browser-panel.tsx +++ b/src/components/panels/memory-browser-panel.tsx @@ -129,7 +129,7 @@ export function MemoryBrowserPanel() { // Enhanced editing functionality const startEditing = () => { setIsEditing(true) - setEditedContent(memoryContent) + setEditedContent(memoryContent ?? '') } const cancelEditing = () => { diff --git a/src/components/panels/token-dashboard-panel.tsx b/src/components/panels/token-dashboard-panel.tsx index a0d2c28..4f46c51 100644 --- a/src/components/panels/token-dashboard-panel.tsx +++ b/src/components/panels/token-dashboard-panel.tsx @@ -201,7 +201,7 @@ export function TokenDashboardPanel() { const getAlerts = () => { const alerts = [] - if (usageStats?.summary.totalCost > 100) { + if (usageStats && usageStats.summary.totalCost !== undefined && usageStats.summary.totalCost > 100) { alerts.push({ type: 'warning', title: 'High Usage Cost', @@ -210,7 +210,7 @@ export function TokenDashboardPanel() { }) } - if (performanceMetrics?.savingsPercentage > 20) { + if (performanceMetrics && performanceMetrics.savingsPercentage !== undefined && performanceMetrics.savingsPercentage > 20) { alerts.push({ type: 'info', title: 'Optimization Opportunity', @@ -219,7 +219,7 @@ export function TokenDashboardPanel() { }) } - if (usageStats?.summary.requestCount > 1000) { + if (usageStats && usageStats.summary.requestCount !== undefined && usageStats.summary.requestCount > 1000) { alerts.push({ type: 'info', title: 'High Request Volume', diff --git a/src/lib/__tests__/auth.test.ts b/src/lib/__tests__/auth.test.ts new file mode 100644 index 0000000..e16dc24 --- /dev/null +++ b/src/lib/__tests__/auth.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect, vi } from 'vitest' + +// Test stubs for auth utilities +// safeCompare will be added by fix/p0-security-critical branch + +describe('requireRole', () => { + it.todo('returns user when authenticated with sufficient role') + it.todo('returns 401 when no authentication provided') + it.todo('returns 403 when role is insufficient') +}) + +describe('safeCompare', () => { + it.todo('returns true for matching strings') + it.todo('returns false for non-matching strings') + it.todo('returns false for different length strings') + it.todo('handles empty strings') +}) diff --git a/tsconfig.json b/tsconfig.json index 0e9e024..b5d5f47 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ ], "allowJs": true, "skipLibCheck": true, - "strict": false, + "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext",