From 0f43e4e750e6bd7badd17d8a102989e3a95e37d9 Mon Sep 17 00:00:00 2001 From: Brad Groux Date: Wed, 28 Jan 2026 04:05:05 -0600 Subject: [PATCH] feat(settings): Replace alert() calls with toast notifications (US-1158) - Add duration support to toast system - Replace all 5 alert() calls in SettingsDialog with toasts - Success toasts auto-dismiss after 3s - Error/warning toasts persist until manually dismissed - Consistent with ManageTab toast patterns --- .../telemetry/events-2026-01-28.ndjson | 22 + server/.veritas-kanban/activity.json | 196 +++++ .../components/settings/SettingsDialog.tsx | 32 +- web/src/hooks/useToast.tsx | 1 + web/src/lib/template-schema.d.ts | 741 ++++++++++++++++++ web/src/lib/template-schema.js | 114 +++ web/src/lib/template-schema.js.map | 1 + 7 files changed, 1102 insertions(+), 5 deletions(-) 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 e8696918..817c0af1 100644 --- a/.veritas-kanban/telemetry/events-2026-01-28.ndjson +++ b/.veritas-kanban/telemetry/events-2026-01-28.ndjson @@ -511,3 +511,25 @@ {"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"} +{"type":"task.status_changed","taskId":"task_20260128_CnogQO","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_cU8ml7qypcWS","timestamp":"2026-01-28T09:09:54.302Z"} +{"type":"task.status_changed","taskId":"task_20260128_-KNOy3","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_yTJBMwcwMG55","timestamp":"2026-01-28T09:10:29.315Z"} +{"type":"task.created","taskId":"task_20260128_-WSaw3","project":"veritas-kanban","status":"todo","id":"evt_0MD7mJBvyDcK","timestamp":"2026-01-28T09:19:20.495Z"} +{"type":"task.created","taskId":"task_20260128_iFFyez","project":"veritas-kanban","status":"todo","id":"evt_v02WIexjF-c0","timestamp":"2026-01-28T09:19:33.479Z"} +{"type":"task.created","taskId":"task_20260128_hQpqum","project":"veritas-kanban","status":"todo","id":"evt_dJryjPl8BCS8","timestamp":"2026-01-28T09:19:48.668Z"} +{"type":"task.created","taskId":"task_20260128_62_v6g","project":"veritas-kanban","status":"todo","id":"evt_GhPlxwdVXEEp","timestamp":"2026-01-28T09:20:01.252Z"} +{"type":"task.created","taskId":"task_20260128_0hOHmd","project":"veritas-kanban","status":"todo","id":"evt_EgigZKRmCB8S","timestamp":"2026-01-28T09:20:12.261Z"} +{"type":"task.created","taskId":"task_20260128_KTgZMx","project":"veritas-kanban","status":"todo","id":"evt_2AwTp6w4FEQe","timestamp":"2026-01-28T09:20:22.111Z"} +{"type":"task.created","taskId":"task_20260128_g86sZV","project":"veritas-kanban","status":"todo","id":"evt_-IY5pwmG3Xt3","timestamp":"2026-01-28T09:20:37.043Z"} +{"type":"task.status_changed","taskId":"task_20260128_8KRThU","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_ZcxGbZLizDuq","timestamp":"2026-01-28T09:20:48.829Z"} +{"type":"task.status_changed","taskId":"task_20260128_8KRThU","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_bZ1gewAss99B","timestamp":"2026-01-28T09:27:59.574Z"} +{"type":"task.archived","taskId":"task_20260128_njhuR8","project":"veritas-kanban","status":"done","id":"evt_7yObxOxuAU2i","timestamp":"2026-01-28T09:28:59.898Z"} +{"type":"task.archived","taskId":"task_20260128_Kbjd3t","project":"veritas-kanban","status":"done","id":"evt_FNSSanv-Jq7S","timestamp":"2026-01-28T09:28:59.903Z"} +{"type":"task.archived","taskId":"task_20260128_itHuxN","project":"veritas-kanban","status":"done","id":"evt__ydcHe_FrYEI","timestamp":"2026-01-28T09:28:59.907Z"} +{"type":"task.archived","taskId":"task_20260128_ndyIRl","project":"veritas-kanban","status":"done","id":"evt_3ACasAI6HT7l","timestamp":"2026-01-28T09:28:59.909Z"} +{"type":"task.archived","taskId":"task_20260128_HaNM-g","project":"veritas-kanban","status":"done","id":"evt_hi2G5vTTvi_U","timestamp":"2026-01-28T09:28:59.910Z"} +{"type":"task.archived","taskId":"task_20260128_vYzfwx","project":"veritas-kanban","status":"done","id":"evt_L92AzZEwRG7o","timestamp":"2026-01-28T09:28:59.912Z"} +{"type":"task.archived","taskId":"task_20260128_DUyAbh","project":"veritas-kanban","status":"done","id":"evt_IAJ3b2hdJPtA","timestamp":"2026-01-28T09:28:59.913Z"} +{"type":"task.archived","taskId":"task_20260128_KbDn0E","project":"veritas-kanban","status":"done","id":"evt_pcQrDaRO5oJo","timestamp":"2026-01-28T09:28:59.927Z"} +{"type":"task.archived","taskId":"task_20260128_gUHeIV","project":"veritas-kanban","status":"done","id":"evt_pVbJN-iGAoy9","timestamp":"2026-01-28T09:28:59.929Z"} +{"type":"task.archived","taskId":"task_20260128_X6nifI","project":"veritas-kanban","status":"done","id":"evt_K4Iqlt-Dw14T","timestamp":"2026-01-28T09:28:59.931Z"} +{"type":"task.status_changed","taskId":"task_20260128_8HU2XD","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_4rB1Mba649ZE","timestamp":"2026-01-28T10:03:19.726Z"} diff --git a/server/.veritas-kanban/activity.json b/server/.veritas-kanban/activity.json index de8a784c..b786a3e5 100644 --- a/server/.veritas-kanban/activity.json +++ b/server/.veritas-kanban/activity.json @@ -1,4 +1,200 @@ [ + { + "id": "activity_1769594599726_5lp9h98h5", + "type": "status_changed", + "taskId": "task_20260128_8HU2XD", + "taskTitle": "US-1158: Replace alert() calls with toast notifications", + "details": { + "from": "todo", + "status": "in-progress" + }, + "timestamp": "2026-01-28T10:03:19.726Z" + }, + { + "id": "activity_1769592539931_twcrqcpt5", + "type": "sprint_archived", + "taskId": "US-1100", + "taskTitle": "US-1100", + "details": { + "taskCount": 10 + }, + "timestamp": "2026-01-28T09:28:59.931Z" + }, + { + "id": "activity_1769592485058_6vdret7hw", + "type": "task_updated", + "taskId": "task_20260128_8KRThU", + "taskTitle": "FEATURE REQUEST: Change Review Column to Blocked", + "timestamp": "2026-01-28T09:28:05.058Z" + }, + { + "id": "activity_1769592479575_p0swcv4xx", + "type": "status_changed", + "taskId": "task_20260128_8KRThU", + "taskTitle": "FEATURE REQUEST: Change Review Column to Blocked", + "details": { + "from": "in-progress", + "status": "done" + }, + "timestamp": "2026-01-28T09:27:59.575Z" + }, + { + "id": "activity_1769592048834_lnlgvfmr9", + "type": "status_changed", + "taskId": "task_20260128_8KRThU", + "taskTitle": "FEATURE REQUEST: Change Review Column to Blocked", + "details": { + "from": "todo", + "status": "in-progress" + }, + "timestamp": "2026-01-28T09:20:48.834Z" + }, + { + "id": "activity_1769592048818_rv4n02og5", + "type": "comment_added", + "taskId": "task_20260128_8KRThU", + "taskTitle": "FEATURE REQUEST: Change Review Column to Blocked", + "details": { + "author": "Veritas", + "preview": "Sprint US-1500 created with 7 stories (US-1501–US-..." + }, + "timestamp": "2026-01-28T09:20:48.818Z" + }, + { + "id": "activity_1769592037043_5b23m5plu", + "type": "task_created", + "taskId": "task_20260128_g86sZV", + "taskTitle": "US-1507: Add blocked reason tracking", + "details": { + "type": "code", + "priority": "medium", + "project": "veritas-kanban" + }, + "timestamp": "2026-01-28T09:20:37.043Z" + }, + { + "id": "activity_1769592022111_s9qvt78ma", + "type": "task_created", + "taskId": "task_20260128_KTgZMx", + "taskTitle": "US-1506: Update test suite — review → blocked references", + "details": { + "type": "code", + "priority": "medium", + "project": "veritas-kanban" + }, + "timestamp": "2026-01-28T09:20:22.111Z" + }, + { + "id": "activity_1769592012261_e2g8dvk3m", + "type": "task_created", + "taskId": "task_20260128_0hOHmd", + "taskTitle": "US-1505: Data migration — convert existing review tasks to blocked", + "details": { + "type": "code", + "priority": "high", + "project": "veritas-kanban" + }, + "timestamp": "2026-01-28T09:20:12.261Z" + }, + { + "id": "activity_1769592001252_wkek4tpfs", + "type": "task_created", + "taskId": "task_20260128_62_v6g", + "taskTitle": "US-1504: Propagate review → blocked across all backend routes and services", + "details": { + "type": "code", + "priority": "high", + "project": "veritas-kanban" + }, + "timestamp": "2026-01-28T09:20:01.252Z" + }, + { + "id": "activity_1769591988668_y93p97rtf", + "type": "task_created", + "taskId": "task_20260128_hQpqum", + "taskTitle": "US-1503: Propagate review → blocked across all frontend components", + "details": { + "type": "code", + "priority": "high", + "project": "veritas-kanban" + }, + "timestamp": "2026-01-28T09:19:48.668Z" + }, + { + "id": "activity_1769591973480_ed1rh84tf", + "type": "task_created", + "taskId": "task_20260128_iFFyez", + "taskTitle": "US-1502: Update Kanban column display — Blocked title + red color", + "details": { + "type": "code", + "priority": "high", + "project": "veritas-kanban" + }, + "timestamp": "2026-01-28T09:19:33.480Z" + }, + { + "id": "activity_1769591960496_c9xm2ql3x", + "type": "task_created", + "taskId": "task_20260128_-WSaw3", + "taskTitle": "US-1501: Rename TaskStatus review → blocked in shared types", + "details": { + "type": "code", + "priority": "high", + "project": "veritas-kanban" + }, + "timestamp": "2026-01-28T09:19:20.496Z" + }, + { + "id": "activity_1769591477776_dv9jz0ymr", + "type": "task_updated", + "taskId": "task_20260128_d47DtQ", + "taskTitle": "FEATURE REQUEST: Host Kimi K2.5 Model In Azure", + "timestamp": "2026-01-28T09:11:17.776Z" + }, + { + "id": "activity_1769591429316_9kwem5ftu", + "type": "status_changed", + "taskId": "task_20260128_-KNOy3", + "taskTitle": "US-1155: Settings performance — memoization & lazy loading", + "details": { + "from": "in-progress", + "status": "done" + }, + "timestamp": "2026-01-28T09:10:29.316Z" + }, + { + "id": "activity_1769591394306_t4h0ljnmv", + "type": "status_changed", + "taskId": "task_20260128_CnogQO", + "taskTitle": "US-1157: Sanitize settings import against prototype pollution", + "details": { + "from": "in-progress", + "status": "done" + }, + "timestamp": "2026-01-28T09:09:54.306Z" + }, + { + "id": "activity_1769591389898_ue1r7w1pp", + "type": "comment_added", + "taskId": "task_20260128_CnogQO", + "taskTitle": "US-1157: Sanitize settings import against prototype pollution", + "details": { + "author": "Veritas", + "preview": "Already implemented in commit 3402051. Added Zod s..." + }, + "timestamp": "2026-01-28T09:09:49.898Z" + }, + { + "id": "activity_1769591349039_t0t1gp5dq", + "type": "comment_added", + "taskId": "task_20260128_-KNOy3", + "taskTitle": "US-1155: Settings performance — memoization & lazy loading", + "details": { + "author": "Veritas", + "preview": "Added React.memo to shared row components, lazy-lo..." + }, + "timestamp": "2026-01-28T09:09:09.039Z" + }, { "id": "activity_1769591243934_a59soco89", "type": "status_changed", diff --git a/web/src/components/settings/SettingsDialog.tsx b/web/src/components/settings/SettingsDialog.tsx index 772db57b..3ccbee01 100644 --- a/web/src/components/settings/SettingsDialog.tsx +++ b/web/src/components/settings/SettingsDialog.tsx @@ -29,6 +29,7 @@ import { useFeatureSettings, useDebouncedFeatureUpdate, } from '@/hooks/useFeatureSettings'; +import { useToast } from '@/hooks/useToast'; import { Settings2, Layout, ListTodo, Cpu, Database, Bell, Archive, Download, Upload, RotateCcw, @@ -95,6 +96,7 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) { const { settings: currentSettings } = useFeatureSettings(); const { debouncedUpdate } = useDebouncedFeatureUpdate(); const settingsFileInputRef = useRef(null); + const { toast } = useToast(); const handleExportSettings = () => { const blob = new Blob([JSON.stringify(currentSettings, null, 2)], { type: 'application/json' }); @@ -116,7 +118,11 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) { const text = await file.text(); const imported = JSON.parse(text); if (!imported || typeof imported !== 'object') { - alert('Invalid settings file: must be a JSON object'); + toast({ + title: 'Import failed', + description: 'Invalid settings file: must be a JSON object', + duration: Infinity, + }); return; } // Validate expected top-level keys @@ -124,7 +130,11 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) { const importedKeys = Object.keys(imported); const unknownKeys = importedKeys.filter(k => !validSections.includes(k)); if (unknownKeys.length > 0) { - alert(`Warning: Unknown sections will be ignored: ${unknownKeys.join(', ')}`); + toast({ + title: 'Warning', + description: `Unknown sections will be ignored: ${unknownKeys.join(', ')}`, + duration: Infinity, + }); } const validPatch: Record = {}; for (const key of importedKeys) { @@ -133,15 +143,27 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) { } } if (Object.keys(validPatch).length === 0) { - alert('No valid settings found in file'); + toast({ + title: 'Import failed', + description: 'No valid settings found in file', + duration: Infinity, + }); return; } if (confirm(`Import ${Object.keys(validPatch).length} setting sections: ${Object.keys(validPatch).join(', ')}?\n\nThis will overwrite current values.`)) { debouncedUpdate(validPatch); - alert('Settings imported successfully!'); + toast({ + title: 'Import complete', + description: 'Settings imported successfully!', + duration: 3000, + }); } } catch (err) { - alert(`Import failed: ${err instanceof Error ? err.message : 'Invalid JSON'}`); + toast({ + title: 'Import failed', + description: err instanceof Error ? err.message : 'Invalid JSON', + duration: Infinity, + }); } finally { if (settingsFileInputRef.current) settingsFileInputRef.current.value = ''; } diff --git a/web/src/hooks/useToast.tsx b/web/src/hooks/useToast.tsx index 368e31ae..68d62e8a 100644 --- a/web/src/hooks/useToast.tsx +++ b/web/src/hooks/useToast.tsx @@ -9,6 +9,7 @@ type ToastProps = { action?: ToastActionElement; open?: boolean; onOpenChange?: (open: boolean) => void; + duration?: number; }; const TOAST_LIMIT = 3; 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":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,wDAAwD;AACxD,MAAM,cAAc,GAAG,CAAC,WAAW,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC;AAEjE;;GAEG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,OAAO,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;GAEG;AACH,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;SACd,GAAG,CAAC,CAAC,EAAE,+BAA+B,CAAC;SACvC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE;QACrC,OAAO,EAAE,uCAAuC;KACjD,CAAC;IACJ,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC1C,CAAC,CAAC,MAAM,EAAE,CAAC;AAEZ;;GAEG;AACH,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;SACd,GAAG,CAAC,CAAC,EAAE,sCAAsC,CAAC;SAC9C,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE;QACrC,OAAO,EAAE,yCAAyC;KACnD,CAAC;IACJ,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;SACd,GAAG,CAAC,CAAC,EAAE,sCAAsC,CAAC;SAC9C,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE;QACrC,OAAO,EAAE,8CAA8C;KACxD,CAAC;IACJ,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC;QACrB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC3B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC9B,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC1C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC7B,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACtB,gBAAgB,EAAE,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,EAAE;IAC3D,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CAC9C,CAAC,CAAC,MAAM,EAAE,CAAC;AAEZ;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,uBAAuB;IAClD,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;SACb,GAAG,CAAC,CAAC,EAAE,2BAA2B,CAAC;SACnC,GAAG,CAAC,GAAG,EAAE,8CAA8C,CAAC;SACxD,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE;QACrC,OAAO,EAAE,uCAAuC;KACjD,CAAC;IACJ,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;SACpB,GAAG,CAAC,GAAG,EAAE,qDAAqD,CAAC;SAC/D,QAAQ,EAAE;SACV,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE;QAC7C,OAAO,EAAE,8CAA8C;KACxD,CAAC;IACJ,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;SACjB,QAAQ,EAAE;SACV,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE;QAC7C,OAAO,EAAE,2CAA2C;KACrD,CAAC;IACJ,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAE3C,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC;QACrB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC3B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC9B,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC1C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KAC7B,CAAC,CAAC,MAAM,EAAE;IAEX,gBAAgB,EAAE,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC;SAC7C,GAAG,CAAC,EAAE,EAAE,sCAAsC,CAAC;SAC/C,QAAQ,EAAE;IAEb,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC;SACpC,GAAG,CAAC,EAAE,EAAE,oCAAoC,CAAC;SAC7C,QAAQ,EAAE;IAEb,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC/B,CAAC,CAAC,MAAM,EAAE;KACR,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;IACf,2CAA2C;IAC3C,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/D,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACrC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;oBACrB,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC,EAAE;IACD,OAAO,EAAE,sFAAsF;CAChG,CAAC,CAAC;AAEL;;GAEG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC;IAC1C,kBAAkB;IAClB,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC;CAC5B,CAAC,CAAC"} \ No newline at end of file