Skip to main content
Glama
hiltonbrown

Next.js MCP Server Template

by hiltonbrown
route.ts8.61 kB
// Xero webhook handler with enhanced security and processing import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { errorHandler, ErrorType, ErrorSeverity } from '@/lib/error-handler'; import { monitoring } from '@/lib/monitoring'; import crypto from 'crypto'; const WEBHOOK_SECRET = process.env.XERO_WEBHOOK_KEY; // In-memory store for processed events (use Redis in production) const processedEvents = new Map<string, number>(); const EVENT_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours export async function POST(request: NextRequest) { const startTime = Date.now(); let requestId = `webhook_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; try { // Extract and validate webhook signature const signature = request.headers.get('x-xero-signature'); if (!signature) { await monitoring.logSystemEvent('webhook_no_signature', { requestId }, 'warning'); return NextResponse.json( { error: 'Missing webhook signature' }, { status: 401 } ); } // Get raw body for signature verification const body = await request.text(); const isValidSignature = verifyWebhookSignature(body, signature); if (!isValidSignature) { await monitoring.logSystemEvent('webhook_invalid_signature', { requestId, signature }, 'error'); return NextResponse.json( { error: 'Invalid webhook signature' }, { status: 401 } ); } // Parse webhook payload let payload; try { payload = JSON.parse(body); } catch (error) { const appError = errorHandler.createError( ErrorType.VALIDATION, 'WEBHOOK_INVALID_JSON', 'Invalid JSON in webhook payload', { severity: ErrorSeverity.MEDIUM, details: { body: body.substring(0, 500) }, requestId } ); return NextResponse.json( errorHandler.toHTTPResponse(appError).json, { status: appError.statusCode } ); } // Validate webhook payload structure if (!payload.events || !Array.isArray(payload.events)) { const appError = errorHandler.createError( ErrorType.VALIDATION, 'WEBHOOK_INVALID_PAYLOAD', 'Invalid webhook payload structure', { severity: ErrorSeverity.MEDIUM, details: payload, requestId } ); return NextResponse.json( errorHandler.toHTTPResponse(appError).json, { status: appError.statusCode } ); } // Process events with idempotency const processedEventIds: string[] = []; const failedEventIds: string[] = []; for (const event of payload.events) { try { const eventId = `${event.resourceId}_${event.eventType}_${event.eventDateUtc}`; // Check for duplicate processing if (isEventProcessed(eventId)) { await monitoring.logSystemEvent('webhook_duplicate_event', { eventId, requestId }, 'info'); continue; } // Process the event await processWebhookEvent(event, requestId); // Mark as processed markEventProcessed(eventId); processedEventIds.push(eventId); } catch (error) { console.error(`Failed to process webhook event:`, error); failedEventIds.push(event.eventId || 'unknown'); await monitoring.logSystemEvent('webhook_event_processing_failed', { eventId: event.eventId, error: error instanceof Error ? error.message : 'Unknown error', requestId }, 'error'); } } // Log successful webhook processing await monitoring.logSystemEvent('webhook_processed', { requestId, totalEvents: payload.events.length, processedEvents: processedEventIds.length, failedEvents: failedEventIds.length, processingTime: Date.now() - startTime }, 'info'); // Return success response return NextResponse.json({ status: 'ok', processed: processedEventIds.length, failed: failedEventIds.length, requestId }); } catch (error) { const appError = errorHandler.createError( ErrorType.SYSTEM, 'WEBHOOK_PROCESSING_ERROR', 'Failed to process webhook', { severity: ErrorSeverity.HIGH, details: error, requestId, originalError: error instanceof Error ? error : undefined } ); await monitoring.logSystemEvent('webhook_processing_error', { requestId, error: appError.message, processingTime: Date.now() - startTime }, 'error'); return NextResponse.json( errorHandler.toHTTPResponse(appError).json, { status: appError.statusCode } ); } } // Verify webhook signature function verifyWebhookSignature(payload: string, signature: string): boolean { if (!WEBHOOK_SECRET) { console.error('XERO_WEBHOOK_KEY not configured'); return false; } try { const expectedSignature = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(payload, 'utf8') .digest('base64'); // Use constant-time comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signature, 'base64'), Buffer.from(expectedSignature, 'base64') ); } catch (error) { console.error('Signature verification error:', error); return false; } } // Check if event has already been processed function isEventProcessed(eventId: string): boolean { const processedAt = processedEvents.get(eventId); if (!processedAt) return false; // Clean up old entries const now = Date.now(); if (now - processedAt > EVENT_EXPIRY) { processedEvents.delete(eventId); return false; } return true; } // Mark event as processed function markEventProcessed(eventId: string): void { processedEvents.set(eventId, Date.now()); // Clean up old entries periodically if (processedEvents.size > 1000) { const now = Date.now(); for (const [id, timestamp] of processedEvents.entries()) { if (now - timestamp > EVENT_EXPIRY) { processedEvents.delete(id); } } } } // Process individual webhook event async function processWebhookEvent(event: any, requestId: string): Promise<void> { const { resourceId, resourceType, eventType, eventDateUtc, tenantId } = event; // Store webhook event in database await db.webhookEvent.create({ data: { eventType, resourceId, resourceType, payload: event, tenantId } }); // Process based on event type switch (eventType) { case 'CREATE': await handleResourceCreated(resourceType, resourceId, tenantId, requestId); break; case 'UPDATE': await handleResourceUpdated(resourceType, resourceId, tenantId, requestId); break; case 'DELETE': await handleResourceDeleted(resourceType, resourceId, tenantId, requestId); break; default: console.log(`Unhandled event type: ${eventType}`); } // Log successful event processing await monitoring.logSystemEvent('webhook_event_processed', { eventType, resourceType, resourceId, tenantId, requestId }, 'info'); } // Event handlers for different resource types async function handleResourceCreated(resourceType: string, resourceId: string, tenantId: string, requestId: string) { // Handle resource creation console.log(`Resource created: ${resourceType} ${resourceId}`); // In a real implementation, you might: // - Update local caches // - Trigger notifications // - Update search indexes // - Queue background jobs } async function handleResourceUpdated(resourceType: string, resourceId: string, tenantId: string, requestId: string) { // Handle resource updates console.log(`Resource updated: ${resourceType} ${resourceId}`); // In a real implementation, you might: // - Invalidate caches // - Update local data stores // - Trigger sync operations } async function handleResourceDeleted(resourceType: string, resourceId: string, tenantId: string, requestId: string) { // Handle resource deletion console.log(`Resource deleted: ${resourceType} ${resourceId}`); // In a real implementation, you might: // - Clean up local data // - Update references // - Trigger cleanup jobs } // Health check endpoint for webhooks export async function GET() { return NextResponse.json({ status: 'ok', message: 'Xero webhook endpoint is active', processedEvents: processedEvents.size, timestamp: new Date().toISOString() }); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/hiltonbrown/xero-mcp-with-next-js'

If you have feedback or need assistance with the MCP directory API, please join our Discord server