Files
Momento/mcp-server/validation.js
Antigravity 0784c94242
Some checks failed
CI / Lint, Test & Build (push) Failing after 57s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): vues structurées tableau/kanban, flashcards et MCP robuste
Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note)
avec activation guidée, tableau éditable, kanban et suppression de colonnes.
Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN.
Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la
robustesse du serveur MCP (config, validation, rate-limit, métriques).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 23:03:16 +00:00

573 lines
14 KiB
JavaScript

/**
* Memento MCP Server - Input Validation
*
* Validates all tool inputs before processing.
* Uses Zod schemas for type-safe validation.
*/
import { z } from 'zod';
// ═══════════════════════════════════════════════════════════════
// Common Validators
// ═══════════════════════════════════════════════════════════════
/**
* Safe string validator (no HTML tags, limited length)
*/
export const safeStringSchema = z
.string()
.max(10000)
.transform((s) => s.trim())
.refine((s) => !/<[^>]*>/g.test(s), {
message: 'String must not contain HTML tags',
});
/**
* ID validator (UUID or custom ID format)
*/
export const idSchema = z
.string()
.min(1)
.max(100)
.regex(/^[a-zA-Z0-9_-]+$/, {
message: 'ID must contain only alphanumeric characters, hyphens, and underscores',
});
/**
* UUID validator
*/
export const uuidSchema = z.string().uuid();
/**
* Title validator (with emoji support)
*/
export const titleSchema = z
.string()
.min(1)
.max(500)
.transform((s) => s.trim());
/**
* Content validator (Markdown-safe, reasonable length)
*/
export const contentSchema = z
.string()
.max(1000000) // 1MB limit
.transform((s) => s.trim());
/**
* Color validator
*/
export const colorSchema = z.enum([
'default',
'red',
'orange',
'yellow',
'green',
'teal',
'blue',
'purple',
'pink',
'gray',
]);
/**
* Note type validator
*/
export const noteTypeSchema = z.enum(['text', 'markdown', 'richtext', 'checklist']);
/**
* Size validator
*/
export const sizeSchema = z.enum(['small', 'medium', 'large']);
/**
* Boolean validator with default
*/
export const boolSchema = (defaultValue = false) =>
z
.boolean()
.optional()
.default(defaultValue)
.transform((v) => typeof v === 'boolean' ? v : defaultValue);
/**
* Date/ISO string validator
*/
export const isoDateSchema = z
.string()
.datetime()
.optional()
.nullable();
/**
* Labels array validator
*/
export const labelsSchema = z
.array(z.string().min(1).max(100))
.max(50)
.optional()
.nullable();
/**
* CheckItems validator
*/
export const checkItemSchema = z.object({
id: z.string().min(1).max(100),
text: z.string().min(1).max(1000),
checked: z.boolean(),
});
export const checkItemsSchema = z.array(checkItemSchema).max(100).optional().nullable();
/**
* Images array validator
*/
export const imagesSchema = z
.array(z.string().url().max(2000))
.max(50)
.optional()
.nullable();
/**
* Links array validator
*/
export const linksSchema = z
.array(z.string().url().max(2000))
.max(50)
.optional()
.nullable();
/**
* Reminder recurrence validator
*/
export const recurrenceSchema = z.enum(['daily', 'weekly', 'monthly', 'yearly']).optional().nullable();
// ═══════════════════════════════════════════════════════════════
// Tool-specific Schemas
// ═══════════════════════════════════════════════════════════════
/**
* create_note input schema
*/
export const createNoteSchema = z.object({
title: titleSchema.optional(),
content: contentSchema,
color: colorSchema.default('default'),
type: noteTypeSchema.default('richtext'),
checkItems: checkItemsSchema,
labels: labelsSchema,
isPinned: boolSchema(false),
isArchived: boolSchema(false),
images: imagesSchema,
links: linksSchema,
reminder: isoDateSchema,
isReminderDone: boolSchema(false),
reminderRecurrence: recurrenceSchema,
reminderLocation: z.string().max(500).optional().nullable(),
isMarkdown: boolSchema(false), // Deprecated
size: sizeSchema.default('small'),
notebookId: idSchema.optional().nullable(),
});
/**
* get_notes input schema
*/
export const getNotesSchema = z.object({
includeArchived: boolSchema(false),
search: z.string().max(500).optional().nullable(),
notebookId: idSchema.optional().nullable(),
fullDetails: boolSchema(false),
limit: z.number().int().min(1).max(500).default(100),
offset: z.number().int().min(0).default(0),
});
/**
* get_note input schema
*/
export const getNoteSchema = z.object({
id: idSchema,
});
/**
* update_note input schema
*/
export const updateNoteSchema = z.object({
id: idSchema,
title: titleSchema.optional(),
content: contentSchema.optional(),
color: colorSchema.optional(),
type: noteTypeSchema.optional(),
checkItems: checkItemsSchema,
labels: labelsSchema,
isPinned: z.boolean().optional(),
isArchived: z.boolean().optional(),
images: imagesSchema,
links: linksSchema,
reminder: isoDateSchema,
isReminderDone: z.boolean().optional(),
reminderRecurrence: recurrenceSchema,
reminderLocation: z.string().max(500).optional().nullable(),
isMarkdown: z.boolean().optional(),
size: sizeSchema.optional(),
notebookId: idSchema.optional().nullable(),
});
/**
* delete_note input schema
*/
export const deleteNoteSchema = z.object({
id: idSchema,
});
/**
* search_notes input schema
*/
export const searchNotesSchema = z.object({
query: z.string().min(1).max(500),
limit: z.number().int().min(1).max(100).default(50),
notebookId: idSchema.optional().nullable(),
includeArchived: boolSchema(false),
});
/**
* move_note input schema
*/
export const moveNoteSchema = z.object({
id: idSchema,
notebookId: idSchema.optional().nullable(),
});
/**
* toggle_pin input schema
*/
export const togglePinSchema = z.object({
id: idSchema,
pinned: z.boolean().optional(),
});
/**
* toggle_archive input schema
*/
export const toggleArchiveSchema = z.object({
id: idSchema,
archived: z.boolean().optional(),
});
/**
* batch_move_notes input schema
*/
export const batchMoveNotesSchema = z.object({
noteIds: z.array(idSchema).min(1).max(100),
notebookId: idSchema.optional().nullable(),
});
/**
* batch_delete_notes input schema
*/
export const batchDeleteNotesSchema = z.object({
noteIds: z.array(idSchema).min(1).max(100),
});
/**
* create_notebook input schema
*/
export const createNotebookSchema = z.object({
name: z.string().min(1).max(200),
color: colorSchema.default('default'),
icon: z.string().max(50).optional().nullable(),
parentId: idSchema.optional().nullable(),
});
/**
* get_notebooks input schema
*/
export const getNotebooksSchema = z.object({
includeHierarchy: boolSchema(false),
includeTrashed: boolSchema(false),
});
/**
* get_notebook input schema
*/
export const getNotebookSchema = z.object({
id: idSchema,
});
/**
* update_notebook input schema
*/
export const updateNotebookSchema = z.object({
id: idSchema,
name: z.string().min(1).max(200).optional(),
color: colorSchema.optional(),
icon: z.string().max(50).optional().nullable(),
parentId: idSchema.optional().nullable(),
});
/**
* delete_notebook input schema
*/
export const deleteNotebookSchema = z.object({
id: idSchema,
force: z.boolean().optional().default(false),
});
/**
* reorder_notebooks input schema
*/
export const reorderNotebooksSchema = z.object({
notebookIds: z.array(idSchema).min(1).max(500),
});
/**
* get_notebook_hierarchy input schema
*/
export const getNotebookHierarchySchema = z.object({
rootId: idSchema.optional().nullable(),
});
/**
* create_label input schema
*/
export const createLabelSchema = z.object({
name: z.string().min(1).max(100),
color: colorSchema.default('default'),
});
/**
* get_labels input schema
*/
export const getLabelsSchema = z.object({
limit: z.number().int().min(1).max(500).default(100),
});
/**
* update_label input schema
*/
export const updateLabelSchema = z.object({
id: idSchema,
name: z.string().min(1).max(100).optional(),
color: colorSchema.optional(),
});
/**
* delete_label input schema
*/
export const deleteLabelSchema = z.object({
id: idSchema,
});
/**
* get_due_reminders input schema
*/
export const getDueRemindersSchema = z.object({
before: isoDateSchema.optional().nullable(),
after: isoDateSchema.optional().nullable(),
includeDone: boolSchema(false),
limit: z.number().int().min(1).max(500).default(100),
});
/**
* export_notes input schema
*/
export const exportNotesSchema = z.object({
notebookId: idSchema.optional().nullable(),
includeArchived: boolSchema(false),
format: z.enum(['json', 'markdown']).default('json'),
});
/**
* import_notes input schema
*/
export const importNotesSchema = z.object({
notes: z.array(
z.object({
title: z.string().optional(),
content: z.string(),
color: colorSchema.optional(),
labels: labelsSchema,
notebookId: idSchema.optional().nullable(),
})
).min(1).max(100),
notebookId: idSchema.optional().nullable(),
overwrite: z.boolean().optional().default(false),
});
// ═══════════════════════════════════════════════════════════════
// Schema Registry
// ═══════════════════════════════════════════════════════════════
/**
* Tool schema registry
*/
export const toolSchemas = {
create_note: createNoteSchema,
get_notes: getNotesSchema,
get_note: getNoteSchema,
update_note: updateNoteSchema,
delete_note: deleteNoteSchema,
search_notes: searchNotesSchema,
move_note: moveNoteSchema,
toggle_pin: togglePinSchema,
toggle_archive: toggleArchiveSchema,
batch_move_notes: batchMoveNotesSchema,
batch_delete_notes: batchDeleteNotesSchema,
create_notebook: createNotebookSchema,
get_notebooks: getNotebooksSchema,
get_notebook: getNotebookSchema,
update_notebook: updateNotebookSchema,
delete_notebook: deleteNotebookSchema,
reorder_notebooks: reorderNotebooksSchema,
get_notebook_hierarchy: getNotebookHierarchySchema,
create_label: createLabelSchema,
get_labels: getLabelsSchema,
update_label: updateLabelSchema,
delete_label: deleteLabelSchema,
get_due_reminders: getDueRemindersSchema,
export_notes: exportNotesSchema,
import_notes: importNotesSchema,
};
// ═══════════════════════════════════════════════════════════════
// Validation Functions
// ═══════════════════════════════════════════════════════════════
/**
* Validate tool input against its schema
*
* @param {string} toolName - Name of the tool
* @param {object} input - Input data to validate
* @returns {{ success: boolean, data?: object, errors?: array }}
*/
export function validateToolInput(toolName, input) {
const schema = toolSchemas[toolName];
if (!schema) {
return {
success: false,
errors: [{ message: `Unknown tool: ${toolName}` }],
};
}
try {
const data = schema.parse(input);
return { success: true, data };
} catch (error) {
if (error instanceof z.ZodError) {
return {
success: false,
errors: error.errors.map((e) => ({
field: e.path.join('.'),
message: e.message,
code: e.code,
})),
};
}
return {
success: false,
errors: [{ message: error.message || 'Validation failed' }],
};
}
}
/**
* Sanitize HTML tags from string
* (Basic sanitization - for production, use a library like sanitize-html)
*/
export function sanitizeHtml(input) {
if (typeof input !== 'string') return input;
return input.replace(/<[^>]*>/g, '');
}
/**
* Sanitize object recursively
*/
export function sanitizeObject(obj, options = {}) {
const { allowedTags = [], allowedAttributes = {} } = options;
if (typeof obj === 'string') {
return sanitizeHtml(obj);
}
if (Array.isArray(obj)) {
return obj.map((item) => sanitizeObject(item, options));
}
if (obj && typeof obj === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = sanitizeObject(value, options);
}
return sanitized;
}
return obj;
}
/**
* Check for potential XSS attacks
*/
export function checkXSS(input) {
const xssPatterns = [
/<script/i,
/javascript:/i,
/on\w+\s*=/i, // onclick=, onload=, etc.
/<iframe/i,
/<object/i,
/<embed/i,
];
const str = typeof input === 'string' ? input : JSON.stringify(input);
for (const pattern of xssPatterns) {
if (pattern.test(str)) {
return true;
}
}
return false;
}
/**
* Validate and sanitize tool input
*
* @param {string} toolName - Name of the tool
* @param {object} input - Input data
* @returns {{ success: boolean, data?: object, errors?: array, sanitized?: object }}
*/
export function validateAndSanitize(toolName, input) {
// First check for XSS
if (checkXSS(input)) {
return {
success: false,
errors: [{ message: 'Input contains potentially malicious content' }],
};
}
// Validate against schema
const validation = validateToolInput(toolName, input);
if (!validation.success) {
return validation;
}
// Sanitize output
const sanitized = sanitizeObject(validation.data);
return {
success: true,
data: sanitized,
sanitized,
};
}
export default {
toolSchemas,
validateToolInput,
validateAndSanitize,
sanitizeHtml,
sanitizeObject,
checkXSS,
};