Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 84 additions & 0 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function Header() {
isSaving,
setWorkflowMetadata,
saveToFile,
saveWorkflow,
loadWorkflow,
previousWorkflowSnapshot,
revertToSnapshot,
Expand All @@ -83,6 +84,7 @@ export function Header() {
isSaving: state.isSaving,
setWorkflowMetadata: state.setWorkflowMetadata,
saveToFile: state.saveToFile,
saveWorkflow: state.saveWorkflow,
loadWorkflow: state.loadWorkflow,
previousWorkflowSnapshot: state.previousWorkflowSnapshot,
revertToSnapshot: state.revertToSnapshot,
Expand All @@ -94,6 +96,7 @@ export function Header() {
const [showProjectModal, setShowProjectModal] = useState(false);
const [projectModalMode, setProjectModalMode] = useState<"new" | "settings">("new");
const fileInputRef = useRef<HTMLInputElement>(null);
const uploadInputRef = useRef<HTMLInputElement>(null);

const isProjectConfigured = !!workflowName;
const canSave = !!(workflowId && workflowName && saveDirectoryPath);
Expand All @@ -119,6 +122,35 @@ export function Header() {
fileInputRef.current?.click();
};

const handleDownloadWorkflow = () => {
saveWorkflow(workflowName || undefined);
};

const handleUploadWorkflow = () => {
uploadInputRef.current?.click();
};

const handleUploadFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;

const reader = new FileReader();
reader.onload = async (event) => {
try {
const workflow = JSON.parse(event.target?.result as string) as WorkflowFile;
if (workflow.version && workflow.nodes && workflow.edges) {
await loadWorkflow(workflow);
} else {
Comment on lines +133 to +143
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Strengthen JSON schema checks before calling loadWorkflow.

At Line 141, truthy checks allow malformed payloads through (e.g., non-array nodes, edges missing source/target/id), which can crash inside loadWorkflow.

🔧 Proposed fix
+  const isValidWorkflowImport = (value: unknown): value is WorkflowFile => {
+    if (!value || typeof value !== "object") return false;
+    const wf = value as Partial<WorkflowFile> & { nodes?: unknown; edges?: unknown };
+    if (wf.version !== 1) return false;
+    if (typeof wf.name !== "string" || !Array.isArray(wf.nodes) || !Array.isArray(wf.edges)) return false;
+
+    const nodesValid = wf.nodes.every((node) => {
+      if (!node || typeof node !== "object") return false;
+      const n = node as { id?: unknown; type?: unknown };
+      return typeof n.id === "string" && typeof n.type === "string";
+    });
+
+    const edgesValid = wf.edges.every((edge) => {
+      if (!edge || typeof edge !== "object") return false;
+      const e = edge as { id?: unknown; source?: unknown; target?: unknown };
+      return (
+        typeof e.id === "string" &&
+        typeof e.source === "string" &&
+        typeof e.target === "string"
+      );
+    });
+
+    return nodesValid && edgesValid;
+  };
+
   const handleUploadFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0];
     if (!file) return;
@@
-        const workflow = JSON.parse(event.target?.result as string) as WorkflowFile;
-        if (workflow.version && workflow.nodes && workflow.edges) {
-          await loadWorkflow(workflow);
+        const parsed = JSON.parse(event.target?.result as string);
+        if (isValidWorkflowImport(parsed)) {
+          await loadWorkflow(parsed);
         } else {
           alert("Invalid workflow file format");
         }

Based on learnings: Validate workflows using validateWorkflow() in workflowStore.ts before execution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Header.tsx` around lines 133 - 143, The uploaded JSON is only
loosely checked before calling loadWorkflow, so replace the current
post-JSON-parse branch in handleUploadFileChange with a validation step: after
parsing into WorkflowFile, call the exported validateWorkflow(...) from
workflowStore.ts and only await loadWorkflow(workflow) if validateWorkflow
returns success; also catch JSON.parse errors and log/report them before
returning, and ensure validateWorkflow verifies nodes is an array and each edge
has source, target, and id so malformed payloads never reach loadWorkflow.

alert("Invalid workflow file format");
}
} catch {
alert("Failed to parse workflow file");
}
};
reader.readAsText(file);
e.target.value = "";
};

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
Expand Down Expand Up @@ -189,6 +221,49 @@ export function Header() {
}
}, [revertToSnapshot]);

const clientWorkflowButtons = (
<div className="flex items-center gap-0.5 ml-1 pl-1 border-l border-neutral-700/50">
<button
onClick={handleDownloadWorkflow}
className="p-1.5 text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 rounded transition-colors"
title="Download workflow"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
</button>
<button
onClick={handleUploadWorkflow}
className="p-1.5 text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 rounded transition-colors"
title="Upload workflow"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"
/>
</svg>
</button>
</div>
);

const settingsButtons = (
<div className="flex items-center gap-0.5 ml-1 pl-1 border-l border-neutral-700/50">
<button
Expand Down Expand Up @@ -233,6 +308,13 @@ export function Header() {
onChange={handleFileChange}
className="hidden"
/>
<input
ref={uploadInputRef}
type="file"
accept=".json"
onChange={handleUploadFileChange}
className="hidden"
/>
<header className="h-11 bg-neutral-900 border-b border-neutral-800 flex items-center justify-between px-4 shrink-0">
<div className="flex items-center gap-2">
<button
Expand Down Expand Up @@ -321,6 +403,7 @@ export function Header() {
</div>

{settingsButtons}
{clientWorkflowButtons}
</>
) : (
<>
Expand Down Expand Up @@ -370,6 +453,7 @@ export function Header() {
</div>

{settingsButtons}
{clientWorkflowButtons}
</>
)}
</div>
Expand Down