From 69572e1d9a305419e9b0f0e5acbab02d6324bfc5 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Thu, 23 Jul 2026 11:39:32 +0530 Subject: [PATCH] fix: fix crypto.randomUUID crash (#573) --- .../components/TransferCallToolConfig.tsx | 3 +- ui/src/app/tools/[toolUuid]/page.tsx | 3 +- ui/src/components/flow/nodes/GenericNode.tsx | 3 +- ui/src/lib/uuid.test.ts | 28 +++++++++++++++++++ ui/src/lib/uuid.ts | 26 +++++++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 ui/src/lib/uuid.test.ts create mode 100644 ui/src/lib/uuid.ts diff --git a/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx b/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx index 47e2bbe8..08ac20b1 100644 --- a/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx +++ b/ui/src/app/tools/[toolUuid]/components/TransferCallToolConfig.tsx @@ -21,6 +21,7 @@ import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; +import { createUuid } from "@/lib/uuid"; import { type ContextDestinationRouteRow, @@ -397,7 +398,7 @@ export function TransferCallToolConfig({ onClick={() => onContextDestinationRoutesChange([ ...contextDestinationRoutes, { - id: crypto.randomUUID(), + id: createUuid(), context_value: "", destination: "", }, diff --git a/ui/src/app/tools/[toolUuid]/page.tsx b/ui/src/app/tools/[toolUuid]/page.tsx index fb919e63..798fd328 100644 --- a/ui/src/app/tools/[toolUuid]/page.tsx +++ b/ui/src/app/tools/[toolUuid]/page.tsx @@ -43,6 +43,7 @@ import { TOOL_DOCUMENTATION_URLS } from "@/constants/documentation"; import { useOrgConfig } from "@/context/OrgConfigContext"; import { detailFromError } from "@/lib/apiError"; import { useAuth } from "@/lib/auth"; +import { createUuid } from "@/lib/uuid"; import { type ContextDestinationRouteRow, @@ -248,7 +249,7 @@ export default function ToolDetailPage() { setTransferContextDestinationRoutes( (config.context_mapping?.routes || []).map((route) => ({ ...route, - id: crypto.randomUUID(), + id: createUuid(), })) ); setTransferFallbackDestination( diff --git a/ui/src/components/flow/nodes/GenericNode.tsx b/ui/src/components/flow/nodes/GenericNode.tsx index f1f5897a..c10fcf83 100644 --- a/ui/src/components/flow/nodes/GenericNode.tsx +++ b/ui/src/components/flow/nodes/GenericNode.tsx @@ -15,6 +15,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { NODE_DOCUMENTATION_URLS } from "@/constants/documentation"; import { useAppConfig } from "@/context/AppConfigContext"; import { cn } from "@/lib/utils"; +import { createUuid } from "@/lib/uuid"; import { resolveWebhookBaseUrl } from "@/lib/webhookUrl"; import { NodeContent } from "./common/NodeContent"; @@ -517,7 +518,7 @@ export const GenericNode = memo(({ data, selected, id, type }: GenericNodeProps) useEffect(() => { if (type !== "trigger") return; if (data.trigger_path) return; - const newPath = crypto.randomUUID(); + const newPath = createUuid(); handleSaveNodeData({ ...data, trigger_path: newPath }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [type]); diff --git a/ui/src/lib/uuid.test.ts b/ui/src/lib/uuid.test.ts new file mode 100644 index 00000000..50c65346 --- /dev/null +++ b/ui/src/lib/uuid.test.ts @@ -0,0 +1,28 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { createUuid } from "./uuid"; + +describe("createUuid", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("uses crypto.randomUUID when it is available", () => { + const randomUUID = vi.fn(() => "27f3a42d-0eb3-43b1-a539-d4d09dd9065e"); + vi.stubGlobal("crypto", { randomUUID }); + + expect(createUuid()).toBe("27f3a42d-0eb3-43b1-a539-d4d09dd9065e"); + expect(randomUUID).toHaveBeenCalledOnce(); + }); + + it("creates a version 4 UUID when crypto.randomUUID is unavailable", () => { + const getRandomValues = vi.fn((bytes: Uint8Array) => { + bytes.fill(0); + return bytes; + }); + vi.stubGlobal("crypto", { getRandomValues }); + + expect(createUuid()).toBe("00000000-0000-4000-8000-000000000000"); + expect(getRandomValues).toHaveBeenCalledOnce(); + }); +}); diff --git a/ui/src/lib/uuid.ts b/ui/src/lib/uuid.ts new file mode 100644 index 00000000..fd8b5b8e --- /dev/null +++ b/ui/src/lib/uuid.ts @@ -0,0 +1,26 @@ +export function createUuid(): string { + const webCrypto = globalThis.crypto; + + if (typeof webCrypto?.randomUUID === "function") { + return webCrypto.randomUUID(); + } + + if (typeof webCrypto?.getRandomValues !== "function") { + throw new Error("Secure random number generation is unavailable"); + } + + const bytes = webCrypto.getRandomValues(new Uint8Array(16)); + + // Mark the generated value as an RFC 4122 version 4 UUID. + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")); + return [ + hex.slice(0, 4).join(""), + hex.slice(4, 6).join(""), + hex.slice(6, 8).join(""), + hex.slice(8, 10).join(""), + hex.slice(10, 16).join(""), + ].join("-"); +}