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>
629 lines
15 KiB
JavaScript
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,
|
|
};
|