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
This commit is contained in:
Brad Groux 2026-01-28 04:05:05 -06:00
parent 0025dc5f47
commit 0f43e4e750
7 changed files with 1102 additions and 5 deletions

View file

@ -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"}

View file

@ -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",

View file

@ -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<HTMLInputElement>(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<string, any> = {};
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 = '';
}

View file

@ -9,6 +9,7 @@ type ToastProps = {
action?: ToastActionElement;
open?: boolean;
onOpenChange?: (open: boolean) => void;
duration?: number;
};
const TOAST_LIMIT = 3;

741
web/src/lib/template-schema.d.ts vendored Normal file
View file

@ -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<z.ZodObject<{
id: z.ZodOptional<z.ZodString>;
name: z.ZodEffects<z.ZodString, string, string>;
description: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
category: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
version: z.ZodOptional<z.ZodNumber>;
taskDefaults: z.ZodObject<{
type: z.ZodOptional<z.ZodString>;
priority: z.ZodOptional<z.ZodString>;
project: z.ZodOptional<z.ZodString>;
descriptionTemplate: z.ZodOptional<z.ZodString>;
agent: z.ZodOptional<z.ZodString>;
}, "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<z.ZodArray<z.ZodObject<{
title: z.ZodEffects<z.ZodString, string, string>;
order: z.ZodOptional<z.ZodNumber>;
}, "strict", z.ZodTypeAny, {
title: string;
order?: number | undefined;
}, {
title: string;
order?: number | undefined;
}>, "many">>;
blueprint: z.ZodOptional<z.ZodArray<z.ZodObject<{
refId: z.ZodEffects<z.ZodString, string, string>;
title: z.ZodEffects<z.ZodString, string, string>;
taskDefaults: z.ZodOptional<z.ZodObject<{
type: z.ZodOptional<z.ZodString>;
priority: z.ZodOptional<z.ZodString>;
project: z.ZodOptional<z.ZodString>;
descriptionTemplate: z.ZodOptional<z.ZodString>;
agent: z.ZodOptional<z.ZodString>;
}, "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<z.ZodArray<z.ZodObject<{
title: z.ZodEffects<z.ZodString, string, string>;
order: z.ZodOptional<z.ZodNumber>;
}, "strict", z.ZodTypeAny, {
title: string;
order?: number | undefined;
}, {
title: string;
order?: number | undefined;
}>, "many">>;
blockedByRefs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
}, "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<z.ZodString>;
updated: z.ZodOptional<z.ZodString>;
}, "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<z.ZodObject<{
id: z.ZodOptional<z.ZodString>;
name: z.ZodEffects<z.ZodString, string, string>;
description: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
category: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
version: z.ZodOptional<z.ZodNumber>;
taskDefaults: z.ZodObject<{
type: z.ZodOptional<z.ZodString>;
priority: z.ZodOptional<z.ZodString>;
project: z.ZodOptional<z.ZodString>;
descriptionTemplate: z.ZodOptional<z.ZodString>;
agent: z.ZodOptional<z.ZodString>;
}, "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<z.ZodArray<z.ZodObject<{
title: z.ZodEffects<z.ZodString, string, string>;
order: z.ZodOptional<z.ZodNumber>;
}, "strict", z.ZodTypeAny, {
title: string;
order?: number | undefined;
}, {
title: string;
order?: number | undefined;
}>, "many">>;
blueprint: z.ZodOptional<z.ZodArray<z.ZodObject<{
refId: z.ZodEffects<z.ZodString, string, string>;
title: z.ZodEffects<z.ZodString, string, string>;
taskDefaults: z.ZodOptional<z.ZodObject<{
type: z.ZodOptional<z.ZodString>;
priority: z.ZodOptional<z.ZodString>;
project: z.ZodOptional<z.ZodString>;
descriptionTemplate: z.ZodOptional<z.ZodString>;
agent: z.ZodOptional<z.ZodString>;
}, "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<z.ZodArray<z.ZodObject<{
title: z.ZodEffects<z.ZodString, string, string>;
order: z.ZodOptional<z.ZodNumber>;
}, "strict", z.ZodTypeAny, {
title: string;
order?: number | undefined;
}, {
title: string;
order?: number | undefined;
}>, "many">>;
blockedByRefs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
}, "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<z.ZodString>;
updated: z.ZodOptional<z.ZodString>;
}, "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<z.ZodEffects<z.ZodObject<{
id: z.ZodOptional<z.ZodString>;
name: z.ZodEffects<z.ZodString, string, string>;
description: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
category: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
version: z.ZodOptional<z.ZodNumber>;
taskDefaults: z.ZodObject<{
type: z.ZodOptional<z.ZodString>;
priority: z.ZodOptional<z.ZodString>;
project: z.ZodOptional<z.ZodString>;
descriptionTemplate: z.ZodOptional<z.ZodString>;
agent: z.ZodOptional<z.ZodString>;
}, "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<z.ZodArray<z.ZodObject<{
title: z.ZodEffects<z.ZodString, string, string>;
order: z.ZodOptional<z.ZodNumber>;
}, "strict", z.ZodTypeAny, {
title: string;
order?: number | undefined;
}, {
title: string;
order?: number | undefined;
}>, "many">>;
blueprint: z.ZodOptional<z.ZodArray<z.ZodObject<{
refId: z.ZodEffects<z.ZodString, string, string>;
title: z.ZodEffects<z.ZodString, string, string>;
taskDefaults: z.ZodOptional<z.ZodObject<{
type: z.ZodOptional<z.ZodString>;
priority: z.ZodOptional<z.ZodString>;
project: z.ZodOptional<z.ZodString>;
descriptionTemplate: z.ZodOptional<z.ZodString>;
agent: z.ZodOptional<z.ZodString>;
}, "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<z.ZodArray<z.ZodObject<{
title: z.ZodEffects<z.ZodString, string, string>;
order: z.ZodOptional<z.ZodNumber>;
}, "strict", z.ZodTypeAny, {
title: string;
order?: number | undefined;
}, {
title: string;
order?: number | undefined;
}>, "many">>;
blockedByRefs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
}, "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<z.ZodString>;
updated: z.ZodOptional<z.ZodString>;
}, "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<typeof TaskTemplateSchema>;
export type TemplateImport = z.infer<typeof TemplateImportSchema>;

View file

@ -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

View file

@ -0,0 +1 @@
{"version":3,"file":"template-schema.js","sourceRoot":"","sources":["template-schema.ts"],"names":[],"mappings":"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"}