From d500a318e5f3379fe2199a07a4265436f3e8f53c Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Fri, 19 Sep 2025 01:19:07 +0000 Subject: [PATCH 1/2] feat(core): add crypto.randomUUID polyfill --- packages/core/package.json | 13 ++++++ .../core/src/submodules/uuid/index.spec.ts | 46 +++++++++++++++++++ packages/core/src/submodules/uuid/index.ts | 19 ++++++++ .../src/submodules/uuid/randomUUID.browser.ts | 1 + .../src/submodules/uuid/randomUUID.native.ts | 2 + .../core/src/submodules/uuid/randomUUID.ts | 4 ++ packages/core/tsconfig.cjs.json | 3 +- packages/core/tsconfig.es.json | 3 +- packages/core/tsconfig.types.json | 3 +- packages/core/uuid.d.ts | 7 +++ packages/core/uuid.js | 6 +++ 11 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/submodules/uuid/index.spec.ts create mode 100644 packages/core/src/submodules/uuid/index.ts create mode 100644 packages/core/src/submodules/uuid/randomUUID.browser.ts create mode 100644 packages/core/src/submodules/uuid/randomUUID.native.ts create mode 100644 packages/core/src/submodules/uuid/randomUUID.ts create mode 100644 packages/core/uuid.d.ts create mode 100644 packages/core/uuid.js diff --git a/packages/core/package.json b/packages/core/package.json index e9fdd028b77..88082e65ab3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -67,6 +67,13 @@ "node": "./dist-cjs/submodules/event-streams/index.js", "import": "./dist-es/submodules/event-streams/index.js", "require": "./dist-cjs/submodules/event-streams/index.js" + }, + "./uuid": { + "types": "./dist-types/submodules/uuid/index.d.ts", + "module": "./dist-es/submodules/uuid/index.js", + "node": "./dist-cjs/submodules/uuid/index.js", + "import": "./dist-es/submodules/uuid/index.js", + "require": "./dist-cjs/submodules/uuid/index.js" } }, "author": { @@ -110,6 +117,8 @@ "./schema.js", "./serde.d.ts", "./serde.js", + "./uuid.d.ts", + "./uuid.js", "dist-*/**" ], "homepage": "https://github.com/smithy-lang/smithy-typescript/tree/main/packages/core", @@ -127,6 +136,10 @@ "rimraf": "3.0.2", "typedoc": "0.23.23" }, + "browser": { + "./dist-es/submodules/uuid/randomUUID": "./dist-es/submodules/uuid/randomUUID.browser" + }, + "react-native": {}, "typedoc": { "entryPoint": "src/index.ts" }, diff --git a/packages/core/src/submodules/uuid/index.spec.ts b/packages/core/src/submodules/uuid/index.spec.ts new file mode 100644 index 00000000000..44b59ca8dab --- /dev/null +++ b/packages/core/src/submodules/uuid/index.spec.ts @@ -0,0 +1,46 @@ +import { getRandomValues } from "crypto"; +import { afterEach, beforeEach, describe, expect, test as it, vi } from "vitest"; + +describe("randomUUID", () => { + afterEach(() => { + vi.resetModules(); + }); + + it("should call native randomUUID when available", async () => { + const mockUUID = "mocked-uuid"; + const nativeRandomUUID = vi.fn(() => mockUUID); + vi.doMock("./randomUUID", () => ({ randomUUID: nativeRandomUUID })); + + const { randomUUID } = await import("./index"); + const uuid = randomUUID(); + + expect(nativeRandomUUID).toHaveBeenCalled(); + expect(uuid).toBe(mockUUID); + }); + + describe("when native randomUUID is not available", () => { + let randomUUID: any; + const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + beforeEach(async () => { + vi.doMock("./randomUUID", () => ({ randomUUID: undefined })); + randomUUID = (await import("./index")).randomUUID; + + // Simulate crypto.getRandomValues in test, as it's expected to be available + global.crypto = { + getRandomValues: getRandomValues, + } as any; + }); + + it("each generation is unique and matches regex", () => { + const uuids = new Set(); + const iterations = 10_000; + for (let i = 0; i < iterations; i++) { + const uuid = randomUUID(); + expect(uuid).toMatch(UUID_REGEX); + uuids.add(uuid); + } + expect(uuids.size).toBe(iterations); + }); + }); +}); diff --git a/packages/core/src/submodules/uuid/index.ts b/packages/core/src/submodules/uuid/index.ts new file mode 100644 index 00000000000..3155b5e0ae6 --- /dev/null +++ b/packages/core/src/submodules/uuid/index.ts @@ -0,0 +1,19 @@ +import { randomUUID as nativeRandomUUID } from "./randomUUID"; + +export const randomUUID = () => { + if (nativeRandomUUID) { + return nativeRandomUUID(); + } + + const rnds = new Uint8Array(16); + crypto.getRandomValues(rnds); + + // Set version (4) and variant (RFC4122) + rnds[6] = (rnds[6] & 0x0f) | 0x40; // version 4 + rnds[8] = (rnds[8] & 0x3f) | 0x80; // variant + + return Array.from(rnds.slice(0, 16)) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + .replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5"); +}; diff --git a/packages/core/src/submodules/uuid/randomUUID.browser.ts b/packages/core/src/submodules/uuid/randomUUID.browser.ts new file mode 100644 index 00000000000..c2760b418cd --- /dev/null +++ b/packages/core/src/submodules/uuid/randomUUID.browser.ts @@ -0,0 +1 @@ +export const randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto); diff --git a/packages/core/src/submodules/uuid/randomUUID.native.ts b/packages/core/src/submodules/uuid/randomUUID.native.ts new file mode 100644 index 00000000000..01da2fdda2d --- /dev/null +++ b/packages/core/src/submodules/uuid/randomUUID.native.ts @@ -0,0 +1,2 @@ +// If user has provided their polyfill, like "react-native-random-uuid" +export const randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto); diff --git a/packages/core/src/submodules/uuid/randomUUID.ts b/packages/core/src/submodules/uuid/randomUUID.ts new file mode 100644 index 00000000000..98cb6455670 --- /dev/null +++ b/packages/core/src/submodules/uuid/randomUUID.ts @@ -0,0 +1,4 @@ +// ToDo: Merge Node.js and browser implementations after dropping support for Node.js 22.x +import crypto from "crypto"; + +export const randomUUID = crypto.randomUUID.bind(crypto); diff --git a/packages/core/tsconfig.cjs.json b/packages/core/tsconfig.cjs.json index e9e9ab91f48..f3fb5789f22 100644 --- a/packages/core/tsconfig.cjs.json +++ b/packages/core/tsconfig.cjs.json @@ -8,7 +8,8 @@ "@smithy/core/protocols": ["./src/submodules/protocols/index.ts"], "@smithy/core/serde": ["./src/submodules/serde/index.ts"], "@smithy/core/schema": ["./src/submodules/schema/index.ts"], - "@smithy/core/event-streams": ["./src/submodules/event-streams/index.ts"] + "@smithy/core/event-streams": ["./src/submodules/event-streams/index.ts"], + "@smithy/core/uuid": ["./src/submodules/uuid/index.ts"] } }, "extends": "../../tsconfig.cjs.json", diff --git a/packages/core/tsconfig.es.json b/packages/core/tsconfig.es.json index 8cab10dd9fb..e197d5d5137 100644 --- a/packages/core/tsconfig.es.json +++ b/packages/core/tsconfig.es.json @@ -9,7 +9,8 @@ "@smithy/core/protocols": ["./src/submodules/protocols/index.ts"], "@smithy/core/serde": ["./src/submodules/serde/index.ts"], "@smithy/core/schema": ["./src/submodules/schema/index.ts"], - "@smithy/core/event-streams": ["./src/submodules/event-streams/index.ts"] + "@smithy/core/event-streams": ["./src/submodules/event-streams/index.ts"], + "@smithy/core/uuid": ["./src/submodules/uuid/index.ts"] } }, "extends": "../../tsconfig.es.json", diff --git a/packages/core/tsconfig.types.json b/packages/core/tsconfig.types.json index 14f10909722..d1e9ee72252 100644 --- a/packages/core/tsconfig.types.json +++ b/packages/core/tsconfig.types.json @@ -8,7 +8,8 @@ "@smithy/core/protocols": ["./src/submodules/protocols/index.ts"], "@smithy/core/serde": ["./src/submodules/serde/index.ts"], "@smithy/core/schema": ["./src/submodules/schema/index.ts"], - "@smithy/core/event-streams": ["./src/submodules/event-streams/index.ts"] + "@smithy/core/event-streams": ["./src/submodules/event-streams/index.ts"], + "@smithy/core/uuid": ["./src/submodules/uuid/index.ts"] } }, "extends": "../../tsconfig.types.json", diff --git a/packages/core/uuid.d.ts b/packages/core/uuid.d.ts new file mode 100644 index 00000000000..b25d7756013 --- /dev/null +++ b/packages/core/uuid.d.ts @@ -0,0 +1,7 @@ +/** + * Do not edit: + * This is a compatibility redirect for contexts that do not understand package.json exports field. + */ +declare module "@smithy/core/uuid" { + export * from "@smithy/core/dist-types/submodules/uuid/index.d"; +} diff --git a/packages/core/uuid.js b/packages/core/uuid.js new file mode 100644 index 00000000000..2835a5092d5 --- /dev/null +++ b/packages/core/uuid.js @@ -0,0 +1,6 @@ + +/** + * Do not edit: + * This is a compatibility redirect for contexts that do not understand package.json exports field. + */ +module.exports = require("./dist-cjs/submodules/uuid/index.js"); From 3fc3a3c2ead1cb28d0494c75652ab0c27c256bce Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:27:03 +0000 Subject: [PATCH 2/2] chore: rename uuid.randomUUID to v4 --- packages/core/src/submodules/uuid/index.ts | 20 +------------------ .../uuid/{index.spec.ts => v4.spec.ts} | 12 +++++------ packages/core/src/submodules/uuid/v4.ts | 19 ++++++++++++++++++ 3 files changed, 26 insertions(+), 25 deletions(-) rename packages/core/src/submodules/uuid/{index.spec.ts => v4.spec.ts} (86%) create mode 100644 packages/core/src/submodules/uuid/v4.ts diff --git a/packages/core/src/submodules/uuid/index.ts b/packages/core/src/submodules/uuid/index.ts index 3155b5e0ae6..d988279e5df 100644 --- a/packages/core/src/submodules/uuid/index.ts +++ b/packages/core/src/submodules/uuid/index.ts @@ -1,19 +1 @@ -import { randomUUID as nativeRandomUUID } from "./randomUUID"; - -export const randomUUID = () => { - if (nativeRandomUUID) { - return nativeRandomUUID(); - } - - const rnds = new Uint8Array(16); - crypto.getRandomValues(rnds); - - // Set version (4) and variant (RFC4122) - rnds[6] = (rnds[6] & 0x0f) | 0x40; // version 4 - rnds[8] = (rnds[8] & 0x3f) | 0x80; // variant - - return Array.from(rnds.slice(0, 16)) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - .replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5"); -}; +export * from "./v4"; \ No newline at end of file diff --git a/packages/core/src/submodules/uuid/index.spec.ts b/packages/core/src/submodules/uuid/v4.spec.ts similarity index 86% rename from packages/core/src/submodules/uuid/index.spec.ts rename to packages/core/src/submodules/uuid/v4.spec.ts index 44b59ca8dab..50e73b63687 100644 --- a/packages/core/src/submodules/uuid/index.spec.ts +++ b/packages/core/src/submodules/uuid/v4.spec.ts @@ -11,20 +11,20 @@ describe("randomUUID", () => { const nativeRandomUUID = vi.fn(() => mockUUID); vi.doMock("./randomUUID", () => ({ randomUUID: nativeRandomUUID })); - const { randomUUID } = await import("./index"); - const uuid = randomUUID(); + const { v4 } = await import("./v4"); + const uuid = v4(); expect(nativeRandomUUID).toHaveBeenCalled(); expect(uuid).toBe(mockUUID); }); describe("when native randomUUID is not available", () => { - let randomUUID: any; + let v4: any; const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; beforeEach(async () => { vi.doMock("./randomUUID", () => ({ randomUUID: undefined })); - randomUUID = (await import("./index")).randomUUID; + v4 = (await import("./v4")).v4; // Simulate crypto.getRandomValues in test, as it's expected to be available global.crypto = { @@ -36,11 +36,11 @@ describe("randomUUID", () => { const uuids = new Set(); const iterations = 10_000; for (let i = 0; i < iterations; i++) { - const uuid = randomUUID(); + const uuid = v4(); expect(uuid).toMatch(UUID_REGEX); uuids.add(uuid); } expect(uuids.size).toBe(iterations); }); }); -}); +}); \ No newline at end of file diff --git a/packages/core/src/submodules/uuid/v4.ts b/packages/core/src/submodules/uuid/v4.ts new file mode 100644 index 00000000000..2d3336de6ac --- /dev/null +++ b/packages/core/src/submodules/uuid/v4.ts @@ -0,0 +1,19 @@ +import { randomUUID } from "./randomUUID"; + +export const v4 = () => { + if (randomUUID) { + return randomUUID(); + } + + const rnds = new Uint8Array(16); + crypto.getRandomValues(rnds); + + // Set version (4) and variant (RFC4122) + rnds[6] = (rnds[6] & 0x0f) | 0x40; // version 4 + rnds[8] = (rnds[8] & 0x3f) | 0x80; // variant + + return Array.from(rnds.slice(0, 16)) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + .replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, "$1-$2-$3-$4-$5"); +}; \ No newline at end of file