From 3c77ef7b01829e0ab7392b55ef826586a79b299d Mon Sep 17 00:00:00 2001 From: Dean Wang Date: Tue, 1 Sep 2026 10:30:40 +0800 Subject: [PATCH] fix(extension): support UUID generation on insecure origins --- .../src/content/record-step-delivery.ts | 3 +- .../src/content/recording/frame-agent.ts | 3 +- .../src/lib/__tests__/random-uuid.test.ts | 30 +++++++++++++++++++ apps/extension/src/lib/random-uuid.ts | 26 ++++++++++++++++ .../src/lib/recording/frame-coordinator.ts | 3 +- 5 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 apps/extension/src/lib/__tests__/random-uuid.test.ts create mode 100644 apps/extension/src/lib/random-uuid.ts diff --git a/apps/extension/src/content/record-step-delivery.ts b/apps/extension/src/content/record-step-delivery.ts index 5099b32..fc55138 100644 --- a/apps/extension/src/content/record-step-delivery.ts +++ b/apps/extension/src/content/record-step-delivery.ts @@ -1,3 +1,4 @@ +import { createRandomUuid } from "@/lib/random-uuid"; import { isAcceptedRecordStepAck, RECORD_STEP, @@ -18,7 +19,7 @@ export class RecordStepDelivery { constructor( requestId: string, send: SendRecordStepMessage = (message) => chrome.runtime.sendMessage(message), - producerId: string = crypto.randomUUID(), + producerId: string = createRandomUuid(), ) { this.#requestId = requestId; this.#producerId = producerId; diff --git a/apps/extension/src/content/recording/frame-agent.ts b/apps/extension/src/content/recording/frame-agent.ts index 68e7b41..5d1b64f 100644 --- a/apps/extension/src/content/recording/frame-agent.ts +++ b/apps/extension/src/content/recording/frame-agent.ts @@ -1,3 +1,4 @@ +import { createRandomUuid } from "@/lib/random-uuid"; import { markRecordingDocument, type RecordingDocumentMarker, @@ -67,7 +68,7 @@ export class RecordFrameAgent { async #start( message: RecordFrameStartMessage, ): Promise<{ ok: true } | { ok: false; error: string }> { - const producerId = crypto.randomUUID(); + const producerId = createRandomUuid(); const root = await waitForDocumentElement(); if (this.#disposed) return { ok: false, error: "recording document is disposed" }; const marker = markRecordingDocument(producerId, root); diff --git a/apps/extension/src/lib/__tests__/random-uuid.test.ts b/apps/extension/src/lib/__tests__/random-uuid.test.ts new file mode 100644 index 0000000..db65ead --- /dev/null +++ b/apps/extension/src/lib/__tests__/random-uuid.test.ts @@ -0,0 +1,30 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createRandomUuid } from "../random-uuid"; + +describe("createRandomUuid", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("uses crypto.randomUUID when it is available", () => { + const randomUUID = vi.fn(() => "00000000-0000-4000-8000-000000000000"); + vi.stubGlobal("crypto", { randomUUID, getRandomValues: vi.fn() }); + + expect(createRandomUuid()).toBe("00000000-0000-4000-8000-000000000000"); + expect(randomUUID).toHaveBeenCalledOnce(); + }); + + it("falls back to getRandomValues and sets UUID v4 bits", () => { + const bytes = new Uint8Array(16).fill(0xff); + const getRandomValues = vi.fn((target: Uint8Array) => { + target.set(bytes); + return target; + }); + vi.stubGlobal("crypto", { getRandomValues }); + + const uuid = createRandomUuid(); + + expect(uuid).toBe("ffffffff-ffff-4fff-bfff-ffffffffffff"); + expect(getRandomValues).toHaveBeenCalledOnce(); + }); +}); diff --git a/apps/extension/src/lib/random-uuid.ts b/apps/extension/src/lib/random-uuid.ts new file mode 100644 index 0000000..ae18e2f --- /dev/null +++ b/apps/extension/src/lib/random-uuid.ts @@ -0,0 +1,26 @@ +/** + * Generate a UUID v4 in browser contexts where randomUUID() is unavailable. + * + * Crypto.randomUUID() is restricted to secure contexts, while + * crypto.getRandomValues() is available to content scripts on ordinary HTTP + * origins. BrowserSkill records many intranet pages served over HTTP, so the + * fallback is required for recording to work on those pages. + */ +export function createRandomUuid(): string { + const cryptoApi = globalThis.crypto; + if (typeof cryptoApi.randomUUID === "function") return cryptoApi.randomUUID(); + + const bytes = new Uint8Array(16); + cryptoApi.getRandomValues(bytes); + bytes[6] = (bytes[6]! & 0x0f) | 0x40; + bytes[8] = (bytes[8]! & 0x3f) | 0x80; + + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); + return [ + hex.slice(0, 8), + hex.slice(8, 12), + hex.slice(12, 16), + hex.slice(16, 20), + hex.slice(20), + ].join("-"); +} diff --git a/apps/extension/src/lib/recording/frame-coordinator.ts b/apps/extension/src/lib/recording/frame-coordinator.ts index 52ca0ad..00dd17c 100644 --- a/apps/extension/src/lib/recording/frame-coordinator.ts +++ b/apps/extension/src/lib/recording/frame-coordinator.ts @@ -1,3 +1,4 @@ +import { createRandomUuid } from "../random-uuid"; import { isRecordFrameQueryMessage, RECORD_FRAME_PORT, @@ -188,7 +189,7 @@ export class RecordFrameCoordinator { const recording = this.#recordings.get(requestId); if (!recording) return true; recording.finishing = true; - const commandId = crypto.randomUUID(); + const commandId = createRandomUuid(); const results = await Promise.all( [...recording.agents.values()].map((agent) => this.#stopAgent(agent, requestId, commandId)), );