UNPKG

leshan-mcp-server

Version:

A standards-compliant MCP server for Leshan LwM2M, exposing Leshan as Model Context Protocol tools.

158 lines (139 loc) 4.09 kB
import { z } from "zod"; /** * Custom validation error class */ export class ValidationError extends Error { constructor(message, field, value) { super(message); this.name = 'ValidationError'; this.field = field; this.value = value; } } /** * Device ID validation schema */ export const deviceIdSchema = z.string() .min(1, "Device ID cannot be empty") .max(64, "Device ID too long") .regex(/^[a-zA-Z0-9._-]+$/, "Device ID contains invalid characters"); /** * LwM2M ID validation schema (for object, instance, resource IDs) */ export const lwm2mIdSchema = z.string() .regex(/^\d+$/, "Must be numeric") .transform(val => parseInt(val, 10)) .refine(val => val >= 0 && val <= 65535, "Must be between 0 and 65535"); /** * Resource value validation schema */ export const resourceValueSchema = z.union([ z.string(), z.number(), z.boolean(), z.array(z.any()), z.object({}).passthrough() ]); /** * Validate LwM2M path parameters * @param {string} deviceId - Device endpoint * @param {string} objectId - Object ID * @param {string} instanceId - Instance ID * @param {string} resourceId - Resource ID * @returns {Object} Validated parameters * @throws {ValidationError} If validation fails */ export function validateLwM2MPath(deviceId, objectId, instanceId, resourceId) { try { const validated = { deviceId: deviceIdSchema.parse(deviceId), objectId: lwm2mIdSchema.parse(objectId), instanceId: lwm2mIdSchema.parse(instanceId), resourceId: lwm2mIdSchema.parse(resourceId) }; return validated; } catch (error) { if (error instanceof z.ZodError) { const firstError = error.errors[0]; throw new ValidationError( `Validation failed for ${firstError.path.join('.')}: ${firstError.message}`, firstError.path.join('.'), firstError.received ); } throw error; } } /** * Validate resource value * @param {any} value - Value to validate * @returns {any} Validated value * @throws {ValidationError} If validation fails */ export function validateResourceValue(value) { try { return resourceValueSchema.parse(value); } catch (error) { if (error instanceof z.ZodError) { throw new ValidationError( `Invalid resource value: ${error.errors[0].message}`, 'value', value ); } throw error; } } /** * Sanitize data for logging (remove sensitive information) * @param {any} data - Data to sanitize * @returns {any} Sanitized data */ export function sanitizeForLogging(data) { if (typeof data !== 'object' || data === null) { return data; } const sensitiveKeys = ['password', 'secret', 'key', 'token', 'credential']; const sanitized = Array.isArray(data) ? [] : {}; for (const [key, value] of Object.entries(data)) { const lowerKey = key.toLowerCase(); const isSensitive = sensitiveKeys.some(sensitive => lowerKey.includes(sensitive)); if (isSensitive) { sanitized[key] = '[REDACTED]'; } else if (typeof value === 'object' && value !== null) { sanitized[key] = sanitizeForLogging(value); } else { sanitized[key] = value; } } return sanitized; } /** * Validate and normalize endpoint path * @param {string} endpoint - Endpoint path * @returns {string} Normalized endpoint */ export function normalizeEndpoint(endpoint) { if (!endpoint || typeof endpoint !== 'string') { throw new ValidationError('Endpoint must be a non-empty string', 'endpoint', endpoint); } // Ensure endpoint starts with / let normalized = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; // Remove double slashes normalized = normalized.replace(/\/+/g, '/'); // Remove trailing slash unless it's the root if (normalized.length > 1 && normalized.endsWith('/')) { normalized = normalized.slice(0, -1); } return normalized; } export default { ValidationError, deviceIdSchema, lwm2mIdSchema, resourceValueSchema, validateLwM2MPath, validateResourceValue, sanitizeForLogging, normalizeEndpoint };