firewalla-mcp-server
Version:
Model Context Protocol (MCP) server for Firewalla MSP API - Provides real-time network monitoring, security analysis, and firewall management through 28 specialized tools compatible with any MCP client
282 lines • 11.6 kB
JavaScript
/**
* Cursor Validation Utilities for Firewalla MCP Server
* Provides comprehensive cursor format validation and standardized error handling
*/
import { ErrorType, createErrorResponse } from './error-handler.js';
import { VALIDATION_CONFIG } from '../config/limits.js';
/**
* Default cursor validation configuration
*/
const DEFAULT_CURSOR_CONFIG = {
maxLength: VALIDATION_CONFIG.CURSOR.maxLength,
allowedPatterns: [
/^[a-zA-Z0-9\-_=+/]+$/, // Base64-like pattern
/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/, // UUID pattern
/^[a-zA-Z0-9]{20,}$/, // Generic alphanumeric (min 20 chars)
],
allowEmpty: false,
strictValidation: true,
checkSuspiciousPatterns: true,
};
/**
* Cursor validation utilities
*/
export class CursorValidator {
/**
* Validate cursor format and return detailed validation result
*
* @param cursor - The cursor value to validate
* @param paramName - Name of the parameter for error messages
* @param config - Validation configuration
* @returns Detailed validation result
*/
static validateCursor(cursor, paramName = 'cursor', config = {}) {
const finalConfig = { ...DEFAULT_CURSOR_CONFIG, ...config };
// Handle null/undefined
if (cursor === null || cursor === undefined) {
if (finalConfig.allowEmpty) {
return {
isValid: true,
errors: [],
sanitizedValue: undefined,
};
}
return {
isValid: false,
errors: [`${paramName} cannot be null or undefined`],
validationError: {
type: 'empty',
message: 'Cursor cannot be null or undefined',
providedValue: cursor,
expectedFormat: 'Valid cursor string',
},
suggestions: [
'Provide a valid cursor from a previous API response',
'Omit the cursor parameter to start from the beginning',
],
};
}
// Type validation
if (typeof cursor !== 'string') {
return {
isValid: false,
errors: [`${paramName} must be a string, got ${typeof cursor}`],
validationError: {
type: 'invalid_type',
message: `Cursor must be a string, got ${typeof cursor}`,
providedValue: cursor,
expectedFormat: 'String',
},
suggestions: [
'Ensure cursor is provided as a string value',
'Check that the cursor is not being converted to another type',
],
};
}
// Empty string validation
if (cursor.trim().length === 0) {
if (finalConfig.allowEmpty) {
return {
isValid: true,
errors: [],
sanitizedValue: undefined,
};
}
return {
isValid: false,
errors: [`${paramName} cannot be empty`],
validationError: {
type: 'empty',
message: 'Cursor cannot be empty',
providedValue: cursor,
expectedFormat: 'Non-empty cursor string',
},
suggestions: [
'Provide a cursor from a previous API response',
'Remove the cursor parameter if not needed',
],
};
}
// Length validation
if (cursor.length > finalConfig.maxLength) {
return {
isValid: false,
errors: [`${paramName} exceeds maximum length of ${finalConfig.maxLength} characters`],
validationError: {
type: 'length',
message: `Cursor exceeds maximum length of ${finalConfig.maxLength} characters`,
providedValue: cursor,
expectedFormat: `String with max ${finalConfig.maxLength} characters`,
},
suggestions: [
'Use a valid cursor from the API response',
'Check for cursor truncation or corruption',
],
};
}
// Pattern validation (if strict validation is enabled)
if (finalConfig.strictValidation) {
const matchesPattern = finalConfig.allowedPatterns.some(pattern => pattern.test(cursor));
if (!matchesPattern) {
return {
isValid: false,
errors: [`${paramName} format is invalid`],
validationError: {
type: 'pattern',
message: 'Cursor format does not match expected patterns',
providedValue: cursor,
expectedFormat: 'Base64-like string, UUID, or alphanumeric string (min 20 chars)',
},
suggestions: [
'Use cursor values returned by the API',
'Check for special characters or encoding issues',
'Verify cursor was not manually constructed',
],
};
}
// Check for suspicious patterns that might indicate security issues (only if enabled)
if (finalConfig.checkSuspiciousPatterns) {
const suspiciousPatterns = [
/[<>'"&]/, // HTML/XML injection attempts
/\.\.\//, // Path traversal attempts
/\s/, // Whitespace (cursors shouldn't contain spaces)
];
// Check for control characters separately to avoid ESLint no-control-regex rule
const hasControlChars = cursor.split('').some(char => {
const code = char.charCodeAt(0);
return (code >= 0 && code <= 31) || code === 127;
});
for (const pattern of suspiciousPatterns) {
if (pattern.test(cursor)) {
return {
isValid: false,
errors: [`${paramName} contains invalid characters`],
validationError: {
type: 'format',
message: 'Cursor contains suspicious or invalid characters',
providedValue: cursor,
expectedFormat: 'Clean alphanumeric cursor string',
},
suggestions: [
'Use only cursors returned by the API',
'Check for encoding or escaping issues',
'Verify cursor source and integrity',
],
};
}
}
// Check for control characters
if (hasControlChars) {
return {
isValid: false,
errors: [`${paramName} contains control characters`],
validationError: {
type: 'format',
message: 'Cursor contains control characters',
providedValue: cursor,
expectedFormat: 'Clean alphanumeric cursor string without control characters',
},
suggestions: [
'Use only cursors returned by the API',
'Check for encoding or character corruption',
'Verify cursor source and integrity',
],
};
}
}
}
return {
isValid: true,
errors: [],
sanitizedValue: cursor.trim(),
};
}
/**
* Create standardized error response for cursor validation failures
*/
static createCursorErrorResponse(toolName, validationResult, paramName = 'cursor') {
const error = validationResult.validationError;
return createErrorResponse(toolName, `Invalid ${paramName} parameter`, ErrorType.VALIDATION_ERROR, {
parameter: paramName,
validation_error: error?.type,
provided_value: error?.providedValue,
expected_format: error?.expectedFormat,
suggestions: validationResult.suggestions,
documentation: 'Cursors should be obtained from previous response pagination metadata',
troubleshooting: [
'Ensure cursor is copied exactly from API response',
'Check for encoding or character corruption',
'Verify cursor has not expired or been modified',
],
}, validationResult.errors);
}
/**
* Quick validation for simple use cases
*
* @param cursor - Cursor to validate
* @param allowEmpty - Whether to allow empty/null cursors
* @returns True if valid, false otherwise
*/
static isValidCursor(cursor, allowEmpty = false) {
const result = this.validateCursor(cursor, 'cursor', { allowEmpty });
return result.isValid;
}
/**
* Sanitize cursor value, returning undefined for invalid cursors
*
* @param cursor - Cursor to sanitize
* @param config - Validation configuration
* @returns Sanitized cursor or undefined
*/
static sanitizeCursor(cursor, config = {}) {
const result = this.validateCursor(cursor, 'cursor', config);
return result.isValid ? result.sanitizedValue : undefined;
}
/**
* Generate example valid cursor for documentation
*/
static getExampleCursor() {
return 'eyJsYXN0X2lkIjoiMTIzNDU2IiwibGFzdF90cyI6MTY0MDk5NTIwMH0';
}
/**
* Get cursor format documentation
*/
static getCursorFormatInfo() {
return {
description: 'Cursors are opaque tokens used for pagination, provided by the API in response metadata',
formats: [
'Base64-encoded strings',
'UUID format (8-4-4-4-12 pattern)',
'Alphanumeric strings (minimum 20 characters)',
],
examples: [
this.getExampleCursor(),
'f47ac10b-58cc-4372-a567-0e02b2c3d479',
'abcd1234567890efghij1234567890',
],
restrictions: [
'Cannot contain whitespace or special characters',
'Maximum length: 1000 characters',
'Must be obtained from API responses',
'Should not be manually constructed',
],
};
}
}
/**
* Convenience function for cursor validation in handlers
*/
export function validateCursorParameter(cursor, toolName, paramName = 'cursor', config) {
const result = CursorValidator.validateCursor(cursor, paramName, config);
if (!result.isValid) {
return {
isValid: false,
errorResponse: CursorValidator.createCursorErrorResponse(toolName, result, paramName),
};
}
return {
isValid: true,
cursor: result.sanitizedValue,
};
}
//# sourceMappingURL=cursor-validator.js.map