Files
Momento/mcp-server/validation.js
Antigravity 80ccc1f6de
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 7m14s
CI / Deploy production (on server) (push) Successful in 1m25s
feat: dashboard Second Brain, essai 7 jours et vérification e-mail
Rendre le dashboard actionnable (inbox, peek, carte mentale), aligner la facturation sur l’essai 7 jours, et bloquer le login e-mail tant que l’adresse n’est pas confirmée.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 07:19:36 +00:00

629 lines
15 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),
});
/**
* 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),
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,
});
/**
* toggle_archive input schema
*/
export const toggleArchiveSchema = z.object({
id: idSchema,
});
/**
* batch_move_notes input schema
*/
export const batchMoveNotesSchema = z.object({
ids: z.array(idSchema).min(1).max(100),
notebookId: idSchema.optional().nullable(),
});
/**
* batch_delete_notes input schema
*/
export const batchDeleteNotesSchema = z.object({
ids: z.array(idSchema).min(1).max(100),
});
/**
* create_notebook input schema
*/
export const createNotebookSchema = z.object({
name: z.string().min(1).max(200),
color: z.string().max(50).default('#3B82F6'),
icon: z.string().max(50).optional().nullable(),
parentId: idSchema.optional().nullable(),
order: z.number().int().optional(),
});
/**
* get_notebooks input schema
*/
export const getNotebooksSchema = z.object({});
/**
* 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: z.string().max(50).optional().nullable(),
icon: z.string().max(50).optional().nullable(),
order: z.number().int().optional(),
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: z.string().max(50).optional().nullable(),
notebookId: idSchema,
});
/**
* get_labels input schema
*/
export const getLabelsSchema = z.object({
notebookId: idSchema.optional().nullable(),
});
/**
* 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({});
/**
* export_notes input schema
*/
export const exportNotesSchema = z.object({});
/**
* import_notes input schema
*/
export const importNotesSchema = z.object({
data: z.object({
version: z.string().optional(),
data: z.object({
notes: z.array(z.any()).optional(),
labels: z.array(z.any()).optional(),
notebooks: z.array(z.any()).optional(),
}).optional(),
}),
});
/**
* find_similar_notes input schema
*/
export const findSimilarNotesSchema = z.object({
id: idSchema,
limit: z.number().int().min(1).max(50).default(10),
threshold: z.number().min(0).max(1).default(0.7),
});
/**
* get_memory_echo_insights input schema
*/
export const getMemoryEchoInsightsSchema = z.object({
includeViewed: boolSchema(false),
includeDismissed: boolSchema(false),
limit: z.number().int().min(1).max(100).default(20),
});
/**
* dismiss_memory_echo_insight input schema
*/
export const dismissMemoryEchoInsightSchema = z.object({
id: idSchema,
viewed: boolSchema(true),
dismissed: boolSchema(true),
});
/**
* get_notes_by_label input schema
*/
export const getNotesByLabelSchema = z.object({
label: z.string().min(1).max(100),
notebookId: idSchema.optional().nullable(),
includeArchived: boolSchema(false),
limit: z.number().int().min(1).max(500).default(100),
});
/**
* get_note_statistics input schema
*/
export const getNoteStatisticsSchema = z.object({
includeArchived: boolSchema(false),
includeTrashed: boolSchema(false),
});
/**
* get_upcoming_reminders input schema
*/
export const getUpcomingRemindersSchema = z.object({
hours: z.number().int().min(1).max(168).default(24),
includeDone: boolSchema(false),
limit: z.number().int().min(1).max(500).default(100),
});
/**
* update_reminder input schema
*/
export const updateReminderSchema = z.object({
id: idSchema,
reminder: isoDateSchema,
isReminderDone: z.boolean().optional(),
reminderRecurrence: recurrenceSchema,
reminderLocation: z.string().max(500).optional().nullable(),
});
// ═══════════════════════════════════════════════════════════════
// 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,
find_similar_notes: findSimilarNotesSchema,
get_memory_echo_insights: getMemoryEchoInsightsSchema,
dismiss_memory_echo_insight: dismissMemoryEchoInsightSchema,
get_notes_by_label: getNotesByLabelSchema,
get_note_statistics: getNoteStatisticsSchema,
get_upcoming_reminders: getUpcomingRemindersSchema,
update_reminder: updateReminderSchema,
};
// ═══════════════════════════════════════════════════════════════
// 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.issues.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,
};