mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
fix: add randomUUID fallback for non-secure contexts (#749)
* fix: fallback when crypto.randomUUID is unavailable * fix(core): remove Math.random UUID fallback and add tests --------- Co-authored-by: Zerion <dev@take-app.local>
This commit is contained in:
@@ -2,6 +2,7 @@ import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
import { createPersistStorage, defaultStorage } from "@multica/core/platform";
|
||||
import { createSafeId } from "@multica/core/utils";
|
||||
import type { DataRouter } from "react-router-dom";
|
||||
import { createTabRouter } from "../routes";
|
||||
|
||||
@@ -69,7 +70,7 @@ export function resolveRouteIcon(pathname: string): string {
|
||||
const DEFAULT_PATH = "/issues";
|
||||
|
||||
function createId(): string {
|
||||
return crypto.randomUUID();
|
||||
return createSafeId();
|
||||
}
|
||||
|
||||
function makeTab(path: string, title: string, icon: string): Tab {
|
||||
|
||||
@@ -52,6 +52,7 @@ import type {
|
||||
ReorderPinsRequest,
|
||||
} from "../types";
|
||||
import { type Logger, noopLogger } from "../logger";
|
||||
import { createRequestId } from "../utils";
|
||||
|
||||
export interface ApiClientOptions {
|
||||
logger?: Logger;
|
||||
@@ -108,7 +109,7 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
private async fetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const rid = crypto.randomUUID().slice(0, 8);
|
||||
const rid = createRequestId();
|
||||
const start = Date.now();
|
||||
const method = init?.method ?? "GET";
|
||||
|
||||
@@ -610,7 +611,7 @@ export class ApiClient {
|
||||
if (opts?.issueId) formData.append("issue_id", opts.issueId);
|
||||
if (opts?.commentId) formData.append("comment_id", opts.commentId);
|
||||
|
||||
const rid = crypto.randomUUID().slice(0, 8);
|
||||
const rid = createRequestId();
|
||||
const start = Date.now();
|
||||
this.logger.info("→ POST /api/upload-file", { rid });
|
||||
|
||||
|
||||
33
packages/core/utils.test.ts
Normal file
33
packages/core/utils.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRequestId, createSafeId, generateUUID } from "./utils";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("utils id helpers", () => {
|
||||
it("generateUUID returns a valid UUID v4", () => {
|
||||
const id = generateUUID();
|
||||
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
||||
});
|
||||
|
||||
it("createSafeId falls back when crypto.randomUUID is unavailable", () => {
|
||||
vi.stubGlobal("crypto", {
|
||||
getRandomValues: (arr: Uint8Array) => {
|
||||
for (let i = 0; i < arr.length; i++) arr[i] = i;
|
||||
return arr;
|
||||
},
|
||||
});
|
||||
|
||||
const id = createSafeId();
|
||||
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
||||
});
|
||||
|
||||
it("createRequestId defaults to length 8 and respects custom length", () => {
|
||||
vi.spyOn(globalThis.crypto, "randomUUID").mockReturnValue("12345678-1234-4abc-8def-1234567890ab");
|
||||
|
||||
expect(createRequestId()).toBe("12345678");
|
||||
expect(createRequestId(12)).toBe("123456781234");
|
||||
});
|
||||
});
|
||||
@@ -8,3 +8,43 @@ export function timeAgo(dateStr: string): string {
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
export function generateUUID(): string {
|
||||
const cryptoObj = globalThis.crypto;
|
||||
|
||||
if (!cryptoObj?.getRandomValues) {
|
||||
throw new Error("Secure UUID generation requires crypto.getRandomValues");
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(16);
|
||||
cryptoObj.getRandomValues(bytes);
|
||||
|
||||
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40; // version 4
|
||||
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80; // variant 1
|
||||
|
||||
const hex = Array.from(bytes, (b) => b.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)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an id that prefers crypto.randomUUID but falls back in non-secure contexts.
|
||||
*/
|
||||
export function createSafeId(): string {
|
||||
const cryptoObj = globalThis.crypto;
|
||||
|
||||
if (cryptoObj?.randomUUID) {
|
||||
try {
|
||||
return cryptoObj.randomUUID();
|
||||
} catch {
|
||||
// Fall through to fallback.
|
||||
}
|
||||
}
|
||||
|
||||
return generateUUID();
|
||||
}
|
||||
|
||||
/** Request id helper used for logs/tracing headers. */
|
||||
export function createRequestId(length = 8): string {
|
||||
return createSafeId().replace(/-/g, "").slice(0, length);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import type { UploadResult } from "@multica/core/hooks/use-file-upload";
|
||||
import { createSafeId } from "@multica/core/utils";
|
||||
|
||||
/** Find and remove a fileCard node by uploadId. */
|
||||
|
||||
@@ -109,7 +110,7 @@ export async function uploadAndInsertFile(
|
||||
}
|
||||
} else {
|
||||
// Non-image: insert skeleton fileCard → upload → finalize with real URL
|
||||
const uploadId = crypto.randomUUID();
|
||||
const uploadId = createSafeId();
|
||||
const cardAttrs = { filename: file.name, href: "", fileSize: file.size, uploading: true, uploadId };
|
||||
const insertContent = { type: "fileCard", attrs: cardAttrs };
|
||||
if (pos !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user