From fa33c433684f7da4941fbfd73e491a84c002d9a7 Mon Sep 17 00:00:00 2001 From: Brad Groux Date: Wed, 28 Jan 2026 03:09:02 -0600 Subject: [PATCH] =?UTF-8?q?US-1155:=20Settings=20performance=20=E2=80=94?= =?UTF-8?q?=20memoization=20&=20lazy=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../telemetry/events-2026-01-28.ndjson | 21 + server/.veritas-kanban/activity.json | 44 ++ server/src/routes/settings.ts | 41 +- server/src/schemas/feature-settings-schema.ts | 74 ++ .../settings/ManagedListManager.tsx | 34 +- .../components/settings/SettingsDialog.tsx | 77 +- .../components/settings/shared/NumberRow.tsx | 5 +- .../components/settings/shared/SettingRow.tsx | 6 +- .../components/settings/shared/ToggleRow.tsx | 5 +- web/src/lib/template-schema.d.ts | 741 ++++++++++++++++++ web/src/lib/template-schema.js | 114 +++ web/src/lib/template-schema.js.map | 1 + 12 files changed, 1117 insertions(+), 46 deletions(-) create mode 100644 server/src/schemas/feature-settings-schema.ts create mode 100644 web/src/lib/template-schema.d.ts create mode 100644 web/src/lib/template-schema.js create mode 100644 web/src/lib/template-schema.js.map diff --git a/.veritas-kanban/telemetry/events-2026-01-28.ndjson b/.veritas-kanban/telemetry/events-2026-01-28.ndjson index 169ee60b..e8696918 100644 --- a/.veritas-kanban/telemetry/events-2026-01-28.ndjson +++ b/.veritas-kanban/telemetry/events-2026-01-28.ndjson @@ -490,3 +490,24 @@ {"type":"task.created","taskId":"task_20260128_W9Henc","status":"todo","id":"evt_t9G3qZADwCc7","timestamp":"2026-01-28T09:05:42.993Z"} {"type":"task.created","taskId":"task_20260128_3SrSde","status":"todo","id":"evt_ljpYJmVetnxw","timestamp":"2026-01-28T09:05:43.001Z"} {"type":"task.archived","taskId":"task_20260128_3SrSde","status":"todo","id":"evt_5l0jQ9llHLbY","timestamp":"2026-01-28T09:05:43.002Z"} +{"type":"task.status_changed","taskId":"task_20260128_6SbOnv","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_EK-e6y771Zso","timestamp":"2026-01-28T09:06:09.459Z"} +{"type":"task.status_changed","taskId":"task_20260128_-KNOy3","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_M7RMF26KsnnP","timestamp":"2026-01-28T09:06:28.250Z"} +{"type":"task.status_changed","taskId":"task_20260128_CnogQO","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_wtwQY0KgQwda","timestamp":"2026-01-28T09:07:23.934Z"} +{"type":"task.created","taskId":"task_20260128_g6jt0d","project":"my-project","status":"todo","id":"evt_OQJ7GpkKhgHw","timestamp":"2026-01-28T09:08:42.546Z"} +{"type":"task.created","taskId":"task_20260128_xefor9","status":"todo","id":"evt_jSQYKFtnuBFY","timestamp":"2026-01-28T09:08:42.549Z"} +{"type":"task.created","taskId":"task_20260128_Lfcgty","status":"todo","id":"evt_p9rXJODNAsFc","timestamp":"2026-01-28T09:08:42.551Z"} +{"type":"task.created","taskId":"task_20260128_fpL_Dd","status":"todo","id":"evt_ZlwfeOttZC0i","timestamp":"2026-01-28T09:08:42.553Z"} +{"type":"task.status_changed","taskId":"task_20260128_fpL_Dd","status":"in-progress","previousStatus":"todo","id":"evt_JL9t1-cWuX3_","timestamp":"2026-01-28T09:08:42.566Z"} +{"type":"task.created","taskId":"task_20260128_jaRqQ3","status":"todo","id":"evt_YprCvvQJN2SO","timestamp":"2026-01-28T09:08:42.568Z"} +{"type":"task.created","taskId":"task_20260128_qZPdbi","status":"todo","id":"evt_XIFHX_Iua532","timestamp":"2026-01-28T09:08:42.570Z"} +{"type":"task.created","taskId":"task_20260128_OFy8h-","status":"todo","id":"evt_-XOvuKgW52ea","timestamp":"2026-01-28T09:08:42.576Z"} +{"type":"task.archived","taskId":"task_20260128_OFy8h-","status":"todo","id":"evt_raxP0Uqjj_ej","timestamp":"2026-01-28T09:08:42.577Z"} +{"type":"task.created","taskId":"task_20260128_p3tQz-","project":"my-project","status":"todo","id":"evt_sv4JD80RG0Mp","timestamp":"2026-01-28T09:08:43.780Z"} +{"type":"task.created","taskId":"task_20260128_lYDQGj","status":"todo","id":"evt_V9D7YuRSiXlx","timestamp":"2026-01-28T09:08:43.783Z"} +{"type":"task.created","taskId":"task_20260128_U1AXxF","status":"todo","id":"evt_IkOyt779Bmx8","timestamp":"2026-01-28T09:08:43.785Z"} +{"type":"task.created","taskId":"task_20260128_u-jBCg","status":"todo","id":"evt_BrnjWauTH2NB","timestamp":"2026-01-28T09:08:43.787Z"} +{"type":"task.status_changed","taskId":"task_20260128_u-jBCg","status":"in-progress","previousStatus":"todo","id":"evt_vPgTJQzmY2-S","timestamp":"2026-01-28T09:08:43.800Z"} +{"type":"task.created","taskId":"task_20260128_Tp9HXB","status":"todo","id":"evt_3hurmcP_cfd4","timestamp":"2026-01-28T09:08:43.803Z"} +{"type":"task.created","taskId":"task_20260128_isj7oa","status":"todo","id":"evt_udJP0CW5DOjq","timestamp":"2026-01-28T09:08:43.805Z"} +{"type":"task.created","taskId":"task_20260128_mY2lH4","status":"todo","id":"evt_NK1qNmhq8BVa","timestamp":"2026-01-28T09:08:43.810Z"} +{"type":"task.archived","taskId":"task_20260128_mY2lH4","status":"todo","id":"evt_xt1yJ4fnooxC","timestamp":"2026-01-28T09:08:43.811Z"} diff --git a/server/.veritas-kanban/activity.json b/server/.veritas-kanban/activity.json index 9b854e8d..de8a784c 100644 --- a/server/.veritas-kanban/activity.json +++ b/server/.veritas-kanban/activity.json @@ -1,4 +1,48 @@ [ + { + "id": "activity_1769591243934_a59soco89", + "type": "status_changed", + "taskId": "task_20260128_CnogQO", + "taskTitle": "US-1157: Sanitize settings import against prototype pollution", + "details": { + "from": "todo", + "status": "in-progress" + }, + "timestamp": "2026-01-28T09:07:23.934Z" + }, + { + "id": "activity_1769591188250_6ff1szybq", + "type": "status_changed", + "taskId": "task_20260128_-KNOy3", + "taskTitle": "US-1155: Settings performance — memoization & lazy loading", + "details": { + "from": "todo", + "status": "in-progress" + }, + "timestamp": "2026-01-28T09:06:28.250Z" + }, + { + "id": "activity_1769591169459_ugp657565", + "type": "status_changed", + "taskId": "task_20260128_6SbOnv", + "taskTitle": "US-1154: Add test coverage for settings features", + "details": { + "from": "in-progress", + "status": "done" + }, + "timestamp": "2026-01-28T09:06:09.459Z" + }, + { + "id": "activity_1769591162356_cdvv56zb6", + "type": "comment_added", + "taskId": "task_20260128_6SbOnv", + "taskTitle": "US-1154: Add test coverage for settings features", + "details": { + "author": "Veritas", + "preview": "Fixed test discovery (worktrees excluded), fixed e..." + }, + "timestamp": "2026-01-28T09:06:02.356Z" + }, { "id": "activity_1769590946523_ekw6m4yic", "type": "task_created", diff --git a/server/src/routes/settings.ts b/server/src/routes/settings.ts index 0f7c59a8..6ada997f 100644 --- a/server/src/routes/settings.ts +++ b/server/src/routes/settings.ts @@ -3,10 +3,28 @@ import { ConfigService } from '../services/config-service.js'; import { getTelemetryService } from '../services/telemetry-service.js'; import { getAttachmentService } from '../services/attachment-service.js'; import type { FeatureSettings } from '@veritas-kanban/shared'; +import { FeatureSettingsPatchSchema } from '../schemas/feature-settings-schema.js'; const router: RouterType = Router(); const configService = new ConfigService(); +// Simple rate limiter +const rateLimiter = new Map(); +const RATE_LIMIT = 10; +const RATE_WINDOW_MS = 60_000; + +function checkRateLimit(ip: string): boolean { + const now = Date.now(); + const entry = rateLimiter.get(ip); + if (!entry || now > entry.resetAt) { + rateLimiter.set(ip, { count: 1, resetAt: now + RATE_WINDOW_MS }); + return true; + } + if (entry.count >= RATE_LIMIT) return false; + entry.count++; + return true; +} + /** * Sync feature settings to affected server-side services. * Called on PATCH and on server startup. @@ -43,12 +61,27 @@ router.get('/features', async (_req, res) => { // PATCH /api/settings/features — deep merge partial updates router.patch('/features', async (req, res) => { try { - const patch = req.body; - if (!patch || typeof patch !== 'object' || Array.isArray(patch)) { - return res.status(400).json({ error: 'Body must be a JSON object with feature settings' }); + // Rate limiting + const clientIp = req.ip || req.socket.remoteAddress || 'unknown'; + if (!checkRateLimit(clientIp)) { + return res.status(429).json({ error: 'Too many requests. Max 10 settings updates per minute.' }); } + + // Validate with Zod — strips unknown keys, rejects dangerous ones + const parseResult = FeatureSettingsPatchSchema.safeParse(req.body); + if (!parseResult.success) { + return res.status(400).json({ + error: 'Invalid settings payload', + details: parseResult.error.issues.map(i => i.message), + }); + } + const patch = parseResult.data; + + if (Object.keys(patch).length === 0) { + return res.status(400).json({ error: 'No valid settings provided' }); + } + const updated = await configService.updateFeatureSettings(patch); - // Sync settings to server-side services syncSettingsToServices(updated); res.json(updated); } catch (error) { diff --git a/server/src/schemas/feature-settings-schema.ts b/server/src/schemas/feature-settings-schema.ts new file mode 100644 index 00000000..5bc42bf6 --- /dev/null +++ b/server/src/schemas/feature-settings-schema.ts @@ -0,0 +1,74 @@ +import { z } from 'zod'; + +// Dangerous keys check +const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype']; +function hasDangerousKeys(obj: unknown): boolean { + if (typeof obj !== 'object' || obj === null) return false; + for (const key of Object.keys(obj)) { + if (DANGEROUS_KEYS.includes(key)) return true; + if (hasDangerousKeys((obj as any)[key])) return true; + } + return false; +} + +const BoardSettingsSchema = z.object({ + showDashboard: z.boolean().optional(), + showArchiveSuggestions: z.boolean().optional(), + cardDensity: z.enum(['normal', 'compact']).optional(), + showPriorityIndicators: z.boolean().optional(), + showProjectBadges: z.boolean().optional(), + showSprintBadges: z.boolean().optional(), + enableDragAndDrop: z.boolean().optional(), +}).strict().optional(); + +const TaskBehaviorSettingsSchema = z.object({ + enableTimeTracking: z.boolean().optional(), + enableSubtaskAutoComplete: z.boolean().optional(), + enableDependencies: z.boolean().optional(), + enableAttachments: z.boolean().optional(), + attachmentMaxFileSize: z.number().int().min(1024).max(100*1024*1024).optional(), + attachmentMaxPerTask: z.number().int().min(1).max(100).optional(), + attachmentMaxTotalSize: z.number().int().min(1024).max(500*1024*1024).optional(), + enableComments: z.boolean().optional(), + defaultPriority: z.enum(['none','low','medium','high','critical']).optional(), +}).strict().optional(); + +const AgentBehaviorSettingsSchema = z.object({ + timeoutMinutes: z.number().int().min(5).max(480).optional(), + autoCommitOnComplete: z.boolean().optional(), + autoCleanupWorktrees: z.boolean().optional(), + enablePreview: z.boolean().optional(), +}).strict().optional(); + +const TelemetrySettingsSchema = z.object({ + enabled: z.boolean().optional(), + retentionDays: z.number().int().min(7).max(365).optional(), + enableTraces: z.boolean().optional(), + enableActivityTracking: z.boolean().optional(), +}).strict().optional(); + +const NotificationSettingsSchema = z.object({ + enabled: z.boolean().optional(), + onTaskComplete: z.boolean().optional(), + onAgentFailure: z.boolean().optional(), + onReviewNeeded: z.boolean().optional(), + channel: z.string().max(200).optional(), +}).strict().optional(); + +const ArchiveSettingsSchema = z.object({ + autoArchiveEnabled: z.boolean().optional(), + autoArchiveAfterDays: z.number().int().min(1).max(365).optional(), +}).strict().optional(); + +export const FeatureSettingsPatchSchema = z.object({ + board: BoardSettingsSchema, + tasks: TaskBehaviorSettingsSchema, + agents: AgentBehaviorSettingsSchema, + telemetry: TelemetrySettingsSchema, + notifications: NotificationSettingsSchema, + archive: ArchiveSettingsSchema, +}).strict().refine(val => !hasDangerousKeys(val), { + message: 'Payload contains forbidden keys (__proto__, constructor, prototype)', +}); + +export type FeatureSettingsPatch = z.infer; diff --git a/web/src/components/settings/ManagedListManager.tsx b/web/src/components/settings/ManagedListManager.tsx index 4e63c499..751177dc 100644 --- a/web/src/components/settings/ManagedListManager.tsx +++ b/web/src/components/settings/ManagedListManager.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, memo, useEffect } from 'react'; import type { ManagedListItem } from '@veritas-kanban/shared'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; @@ -52,7 +52,7 @@ interface SortableItemProps { canDeleteCheck?: (id: string) => Promise<{ allowed: boolean; referenceCount: number; isDefault: boolean }>; } -function SortableItem({ +const SortableItem = memo(function SortableItem({ item, onUpdate, onDelete, @@ -194,7 +194,7 @@ function SortableItem({ ); -} +}) as (props: SortableItemProps) => React.JSX.Element; export function ManagedListManager({ title, @@ -210,6 +210,7 @@ export function ManagedListManager({ }: ManagedListManagerProps) { const [newItemLabel, setNewItemLabel] = useState(''); const [isCreating, setIsCreating] = useState(false); + const [localItems, setLocalItems] = useState(items); const sensors = useSensors( useSensor(PointerSensor), @@ -218,17 +219,30 @@ export function ManagedListManager({ }) ); - const handleDragEnd = async (event: DragEndEvent) => { + // Sync local state with incoming items + useEffect(() => { + setLocalItems(items); + }, [items]); + + const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { - const oldIndex = items.findIndex((item) => item.id === active.id); - const newIndex = items.findIndex((item) => item.id === over.id); + const oldIndex = localItems.findIndex((item) => item.id === active.id); + const newIndex = localItems.findIndex((item) => item.id === over.id); - const reordered = arrayMove(items, oldIndex, newIndex); + const reordered = arrayMove(localItems, oldIndex, newIndex); const orderedIds = reordered.map((item) => item.id); - await onReorder(orderedIds); + // Optimistic update + setLocalItems(reordered); + + // Fire onReorder in background + onReorder(orderedIds).catch((error) => { + // Rollback on error + console.error('Failed to reorder items:', error); + setLocalItems(items); + }); } }; @@ -261,10 +275,10 @@ export function ManagedListManager({ onDragEnd={handleDragEnd} > item.id)} + items={localItems.map((item) => item.id)} strategy={verticalListSortingStrategy} > - {items.map((item) => ( + {localItems.map((item) => ( import('./tabs/GeneralTab').then(m => ({ default: m.GeneralTab }))); +const LazyBoardTab = lazy(() => import('./tabs/BoardTab').then(m => ({ default: m.BoardTab }))); +const LazyTasksTab = lazy(() => import('./tabs/TasksTab').then(m => ({ default: m.TasksTab }))); +const LazyAgentsTab = lazy(() => import('./tabs/AgentsTab').then(m => ({ default: m.AgentsTab }))); +const LazyDataTab = lazy(() => import('./tabs/DataTab').then(m => ({ default: m.DataTab }))); +const LazyNotificationsTab = lazy(() => import('./tabs/NotificationsTab').then(m => ({ default: m.NotificationsTab }))); +const LazyManageTab = lazy(() => import('./tabs/ManageTab').then(m => ({ default: m.ManageTab }))); + +// ============ Tab Skeleton ============ + +function TabSkeleton() { + return ( +
+ +
+ + + +
+
+ ); +} + // ============ Tab Configuration ============ type TabId = 'general' | 'board' | 'tasks' | 'agents' | 'data' | 'notifications' | 'manage'; @@ -149,22 +165,31 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) { }, [activeTab]); const renderTab = () => { - switch (activeTab) { - case 'general': - return ; - case 'board': - return ; - case 'tasks': - return ; - case 'agents': - return ; - case 'data': - return ; - case 'notifications': - return ; - case 'manage': - return ; - } + return ( + }> + {activeTab === 'general' && ( + + )} + {activeTab === 'board' && ( + + )} + {activeTab === 'tasks' && ( + + )} + {activeTab === 'agents' && ( + + )} + {activeTab === 'data' && ( + + )} + {activeTab === 'notifications' && ( + + )} + {activeTab === 'manage' && ( + + )} + + ); }; return ( diff --git a/web/src/components/settings/shared/NumberRow.tsx b/web/src/components/settings/shared/NumberRow.tsx index 9e2f2a92..b9f8567b 100644 --- a/web/src/components/settings/shared/NumberRow.tsx +++ b/web/src/components/settings/shared/NumberRow.tsx @@ -1,7 +1,8 @@ +import { memo } from 'react'; import { Input } from '@/components/ui/input'; import { SettingRow } from './SettingRow'; -export function NumberRow({ label, description, value, onChange, min, max, unit }: { +export const NumberRow = memo(function NumberRow({ label, description, value, onChange, min, max, unit }: { label: string; description?: string; value: number; @@ -31,4 +32,4 @@ export function NumberRow({ label, description, value, onChange, min, max, unit ); -} +}); diff --git a/web/src/components/settings/shared/SettingRow.tsx b/web/src/components/settings/shared/SettingRow.tsx index ff4ad3ac..88fc1751 100644 --- a/web/src/components/settings/shared/SettingRow.tsx +++ b/web/src/components/settings/shared/SettingRow.tsx @@ -1,4 +1,6 @@ -export function SettingRow({ label, description, children }: { +import { memo } from 'react'; + +export const SettingRow = memo(function SettingRow({ label, description, children }: { label: string; description?: string; children: React.ReactNode; @@ -14,4 +16,4 @@ export function SettingRow({ label, description, children }: {
{children}
); -} +}); diff --git a/web/src/components/settings/shared/ToggleRow.tsx b/web/src/components/settings/shared/ToggleRow.tsx index ed0e2768..24705cce 100644 --- a/web/src/components/settings/shared/ToggleRow.tsx +++ b/web/src/components/settings/shared/ToggleRow.tsx @@ -1,7 +1,8 @@ +import { memo } from 'react'; import { Switch } from '@/components/ui/switch'; import { SettingRow } from './SettingRow'; -export function ToggleRow({ label, description, checked, onCheckedChange }: { +export const ToggleRow = memo(function ToggleRow({ label, description, checked, onCheckedChange }: { label: string; description?: string; checked: boolean; @@ -12,4 +13,4 @@ export function ToggleRow({ label, description, checked, onCheckedChange }: { ); -} +}); diff --git a/web/src/lib/template-schema.d.ts b/web/src/lib/template-schema.d.ts new file mode 100644 index 00000000..dc705ce1 --- /dev/null +++ b/web/src/lib/template-schema.d.ts @@ -0,0 +1,741 @@ +/** + * Zod validation schema for task templates + * Enforces strict validation, size limits, and security checks + */ +import { z } from 'zod'; +/** + * Task template schema with strict validation + */ +export declare const TaskTemplateSchema: z.ZodEffects; + name: z.ZodEffects; + description: z.ZodEffects, string | undefined, string | undefined>; + category: z.ZodEffects, string | undefined, string | undefined>; + version: z.ZodOptional; + taskDefaults: z.ZodObject<{ + type: z.ZodOptional; + priority: z.ZodOptional; + project: z.ZodOptional; + descriptionTemplate: z.ZodOptional; + agent: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }>; + subtaskTemplates: z.ZodOptional; + order: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + title: string; + order?: number | undefined; + }, { + title: string; + order?: number | undefined; + }>, "many">>; + blueprint: z.ZodOptional; + title: z.ZodEffects; + taskDefaults: z.ZodOptional; + priority: z.ZodOptional; + project: z.ZodOptional; + descriptionTemplate: z.ZodOptional; + agent: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }>>; + subtaskTemplates: z.ZodOptional; + order: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + title: string; + order?: number | undefined; + }, { + title: string; + order?: number | undefined; + }>, "many">>; + blockedByRefs: z.ZodOptional>; + }, "strict", z.ZodTypeAny, { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }, { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }>, "many">>; + created: z.ZodOptional; + updated: z.ZodOptional; +}, "strict", z.ZodTypeAny, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}>, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}>; +/** + * Schema for importing templates (single or array) + */ +export declare const TemplateImportSchema: z.ZodUnion<[z.ZodEffects; + name: z.ZodEffects; + description: z.ZodEffects, string | undefined, string | undefined>; + category: z.ZodEffects, string | undefined, string | undefined>; + version: z.ZodOptional; + taskDefaults: z.ZodObject<{ + type: z.ZodOptional; + priority: z.ZodOptional; + project: z.ZodOptional; + descriptionTemplate: z.ZodOptional; + agent: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }>; + subtaskTemplates: z.ZodOptional; + order: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + title: string; + order?: number | undefined; + }, { + title: string; + order?: number | undefined; + }>, "many">>; + blueprint: z.ZodOptional; + title: z.ZodEffects; + taskDefaults: z.ZodOptional; + priority: z.ZodOptional; + project: z.ZodOptional; + descriptionTemplate: z.ZodOptional; + agent: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }>>; + subtaskTemplates: z.ZodOptional; + order: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + title: string; + order?: number | undefined; + }, { + title: string; + order?: number | undefined; + }>, "many">>; + blockedByRefs: z.ZodOptional>; + }, "strict", z.ZodTypeAny, { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }, { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }>, "many">>; + created: z.ZodOptional; + updated: z.ZodOptional; +}, "strict", z.ZodTypeAny, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}>, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}>, z.ZodArray; + name: z.ZodEffects; + description: z.ZodEffects, string | undefined, string | undefined>; + category: z.ZodEffects, string | undefined, string | undefined>; + version: z.ZodOptional; + taskDefaults: z.ZodObject<{ + type: z.ZodOptional; + priority: z.ZodOptional; + project: z.ZodOptional; + descriptionTemplate: z.ZodOptional; + agent: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }>; + subtaskTemplates: z.ZodOptional; + order: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + title: string; + order?: number | undefined; + }, { + title: string; + order?: number | undefined; + }>, "many">>; + blueprint: z.ZodOptional; + title: z.ZodEffects; + taskDefaults: z.ZodOptional; + priority: z.ZodOptional; + project: z.ZodOptional; + descriptionTemplate: z.ZodOptional; + agent: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }, { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }>>; + subtaskTemplates: z.ZodOptional; + order: z.ZodOptional; + }, "strict", z.ZodTypeAny, { + title: string; + order?: number | undefined; + }, { + title: string; + order?: number | undefined; + }>, "many">>; + blockedByRefs: z.ZodOptional>; + }, "strict", z.ZodTypeAny, { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }, { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }>, "many">>; + created: z.ZodOptional; + updated: z.ZodOptional; +}, "strict", z.ZodTypeAny, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}>, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}, { + name: string; + taskDefaults: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + }; + id?: string | undefined; + description?: string | undefined; + created?: string | undefined; + updated?: string | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + category?: string | undefined; + blueprint?: { + title: string; + refId: string; + taskDefaults?: { + type?: string | undefined; + project?: string | undefined; + priority?: string | undefined; + agent?: string | undefined; + descriptionTemplate?: string | undefined; + } | undefined; + subtaskTemplates?: { + title: string; + order?: number | undefined; + }[] | undefined; + blockedByRefs?: string[] | undefined; + }[] | undefined; + version?: number | undefined; +}>, "many">]>; +/** + * Type exports + */ +export type ValidatedTemplate = z.infer; +export type TemplateImport = z.infer; diff --git a/web/src/lib/template-schema.js b/web/src/lib/template-schema.js new file mode 100644 index 00000000..a73f7078 --- /dev/null +++ b/web/src/lib/template-schema.js @@ -0,0 +1,114 @@ +/** + * Zod validation schema for task templates + * Enforces strict validation, size limits, and security checks + */ +import { z } from 'zod'; +// Dangerous keys that could lead to prototype pollution +const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype']; +/** + * Check if a string contains dangerous keys + */ +function hasDangerousKeys(str) { + return DANGEROUS_KEYS.some(key => str.includes(key)); +} +/** + * Subtask template schema + */ +const SubtaskTemplateSchema = z.object({ + title: z.string() + .min(1, 'Subtask title cannot be empty') + .refine(val => !hasDangerousKeys(val), { + message: 'Subtask title contains forbidden keys', + }), + order: z.number().int().min(0).optional(), +}).strict(); +/** + * Blueprint task schema + */ +const BlueprintTaskSchema = z.object({ + refId: z.string() + .min(1, 'Blueprint task refId cannot be empty') + .refine(val => !hasDangerousKeys(val), { + message: 'Blueprint refId contains forbidden keys', + }), + title: z.string() + .min(1, 'Blueprint task title cannot be empty') + .refine(val => !hasDangerousKeys(val), { + message: 'Blueprint task title contains forbidden keys', + }), + taskDefaults: z.object({ + type: z.string().optional(), + priority: z.string().optional(), + project: z.string().optional(), + descriptionTemplate: z.string().optional(), + agent: z.string().optional(), + }).strict().optional(), + subtaskTemplates: z.array(SubtaskTemplateSchema).optional(), + blockedByRefs: z.array(z.string()).optional(), +}).strict(); +/** + * Task template schema with strict validation + */ +export const TaskTemplateSchema = z.object({ + id: z.string().optional(), // Optional for imports + name: z.string() + .min(1, 'Template name is required') + .max(100, 'Template name must be 100 characters or less') + .refine(val => !hasDangerousKeys(val), { + message: 'Template name contains forbidden keys', + }), + description: z.string() + .max(500, 'Template description must be 500 characters or less') + .optional() + .refine(val => !val || !hasDangerousKeys(val), { + message: 'Template description contains forbidden keys', + }), + category: z.string() + .optional() + .refine(val => !val || !hasDangerousKeys(val), { + message: 'Template category contains forbidden keys', + }), + version: z.number().int().min(0).optional(), + taskDefaults: z.object({ + type: z.string().optional(), + priority: z.string().optional(), + project: z.string().optional(), + descriptionTemplate: z.string().optional(), + agent: z.string().optional(), + }).strict(), + subtaskTemplates: z.array(SubtaskTemplateSchema) + .max(50, 'Maximum 50 subtask templates allowed') + .optional(), + blueprint: z.array(BlueprintTaskSchema) + .max(20, 'Maximum 20 blueprint tasks allowed') + .optional(), + created: z.string().optional(), + updated: z.string().optional(), +}).strict() + .refine((data) => { + // Validate blueprint dependency references + if (!data.blueprint || data.blueprint.length === 0) { + return true; + } + const refIds = new Set(data.blueprint.map(task => task.refId)); + for (const task of data.blueprint) { + if (task.blockedByRefs) { + for (const ref of task.blockedByRefs) { + if (!refIds.has(ref)) { + return false; + } + } + } + } + return true; +}, { + message: 'Blueprint dependency references must point to valid refIds within the same blueprint', +}); +/** + * Schema for importing templates (single or array) + */ +export const TemplateImportSchema = z.union([ + TaskTemplateSchema, + z.array(TaskTemplateSchema), +]); +//# sourceMappingURL=template-schema.js.map \ No newline at end of file diff --git a/web/src/lib/template-schema.js.map b/web/src/lib/template-schema.js.map new file mode 100644 index 00000000..62f9e825 --- /dev/null +++ b/web/src/lib/template-schema.js.map @@ -0,0 +1 @@ +{"version":3,"file":"template-schema.js","sourceRoot":"","sources":["template-schema.ts"],"names":[],"mappings":"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"} \ No newline at end of file