From d24faf3a05a29d5ac6b39acc247fe2869efd1bd9 Mon Sep 17 00:00:00 2001 From: Ziyak Date: Thu, 25 Sep 2025 17:04:23 +0530 Subject: [PATCH 1/3] fix(randomUUID): add fallback when crypto.randomUUID is unavailable (HTTP/older envs) In some environments (non-secure origins like http, older browsers, certain test runners), `crypto.randomUUID` is not available, causing runtime errors. This change preserves the existing fast path when `crypto.randomUUID` exists, and adds a RFC4122 v4-shaped fallback for other cases: - Detects availability via: typeof crypto !== 'undefined' && crypto.randomUUID - Uses `crypto.randomUUID.bind(crypto)` when present - Otherwise formats a UUID using 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(...) with Math.random, ensuring version (4) and variant (8|9|a|b) bits - Keeps strict typing with template-literal UUID type for DX No breaking changes. --- packages/agents-core/src/shims/shims-browser.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/agents-core/src/shims/shims-browser.ts b/packages/agents-core/src/shims/shims-browser.ts index 231e8c8e..e1361d11 100644 --- a/packages/agents-core/src/shims/shims-browser.ts +++ b/packages/agents-core/src/shims/shims-browser.ts @@ -82,7 +82,14 @@ export class BrowserEventEmitter< export { BrowserEventEmitter as RuntimeEventEmitter }; -export const randomUUID = crypto.randomUUID.bind(crypto); +export const randomUUID: () => `${string}-${string}-${string}-${string}-${string}` = + (typeof crypto !== 'undefined' && crypto.randomUUID) + ? crypto.randomUUID.bind(crypto) + : () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }) as `${string}-${string}-${string}-${string}-${string}`; export const Readable = class Readable { constructor() {} pipeTo( From 58e83159e83d0e941b39fe474b393b7f029dd11a Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Thu, 25 Sep 2025 20:47:10 +0900 Subject: [PATCH 2/3] Create lemon-falcons-hunt.md --- .changeset/lemon-falcons-hunt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lemon-falcons-hunt.md diff --git a/.changeset/lemon-falcons-hunt.md b/.changeset/lemon-falcons-hunt.md new file mode 100644 index 00000000..3473dabf --- /dev/null +++ b/.changeset/lemon-falcons-hunt.md @@ -0,0 +1,5 @@ +--- +"@openai/agents-core": patch +--- + +fix(randomUUID): add fallback when crypto.randomUUID is unavailable From 62d8648ab35e9ec6ca50dbd77fe7ad893fdca4cb Mon Sep 17 00:00:00 2001 From: Ziyak Date: Thu, 25 Sep 2025 17:33:39 +0530 Subject: [PATCH 3/3] test(randomUUID): cover native and fallback paths; assert RFC4122 v4 shape --- .../agents-core/src/shims/shims-browser.ts | 20 ++- .../test/shims/browser-event-emitter.test.ts | 55 -------- .../test/shims/browser-shims.test.ts | 120 ++++++++++++++++++ 3 files changed, 133 insertions(+), 62 deletions(-) delete mode 100644 packages/agents-core/test/shims/browser-event-emitter.test.ts create mode 100644 packages/agents-core/test/shims/browser-shims.test.ts diff --git a/packages/agents-core/src/shims/shims-browser.ts b/packages/agents-core/src/shims/shims-browser.ts index e1361d11..55e5397f 100644 --- a/packages/agents-core/src/shims/shims-browser.ts +++ b/packages/agents-core/src/shims/shims-browser.ts @@ -83,13 +83,19 @@ export class BrowserEventEmitter< export { BrowserEventEmitter as RuntimeEventEmitter }; export const randomUUID: () => `${string}-${string}-${string}-${string}-${string}` = - (typeof crypto !== 'undefined' && crypto.randomUUID) - ? crypto.randomUUID.bind(crypto) - : () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - const r = Math.random() * 16 | 0; - const v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }) as `${string}-${string}-${string}-${string}-${string}`; + () => { + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace( + /[xy]/g, + function (c) { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }, + ) as `${string}-${string}-${string}-${string}-${string}`; + }; export const Readable = class Readable { constructor() {} pipeTo( diff --git a/packages/agents-core/test/shims/browser-event-emitter.test.ts b/packages/agents-core/test/shims/browser-event-emitter.test.ts deleted file mode 100644 index a596366a..00000000 --- a/packages/agents-core/test/shims/browser-event-emitter.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import { BrowserEventEmitter } from '../../src/shims/shims-browser'; - -describe('BrowserEventEmitter', () => { - test('off removes previously registered listener', () => { - const emitter = new BrowserEventEmitter<{ foo: [string] }>(); - const calls: string[] = []; - - const handler = (value: string) => { - calls.push(value); - }; - - emitter.on('foo', handler); - emitter.emit('foo', 'first'); - emitter.off('foo', handler); - emitter.emit('foo', 'second'); - - expect(calls).toEqual(['first']); - }); - - test('once triggers listener only once', () => { - const emitter = new BrowserEventEmitter<{ foo: [string] }>(); - let callCount = 0; - - emitter.once('foo', () => { - callCount += 1; - }); - - emitter.emit('foo', 'first'); - emitter.emit('foo', 'second'); - - expect(callCount).toBe(1); - }); - - test('multiple identical listeners fire for each registration and are removed by off', () => { - const emitter = new BrowserEventEmitter<{ foo: [string] }>(); - const calls: string[] = []; - - const handler = (value: string) => { - calls.push(value); - }; - - emitter.on('foo', handler); - emitter.on('foo', handler); - - emitter.emit('foo', 'first'); - expect(calls).toEqual(['first', 'first']); - - emitter.off('foo', handler); - emitter.emit('foo', 'second'); - - expect(calls).toEqual(['first', 'first']); - }); -}); diff --git a/packages/agents-core/test/shims/browser-shims.test.ts b/packages/agents-core/test/shims/browser-shims.test.ts new file mode 100644 index 00000000..6fdb6979 --- /dev/null +++ b/packages/agents-core/test/shims/browser-shims.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, test, vi } from 'vitest'; + +import { BrowserEventEmitter, randomUUID } from '../../src/shims/shims-browser'; + +describe('BrowserEventEmitter', () => { + test('off removes previously registered listener', () => { + const emitter = new BrowserEventEmitter<{ foo: [string] }>(); + const calls: string[] = []; + + const handler = (value: string) => { + calls.push(value); + }; + + emitter.on('foo', handler); + emitter.emit('foo', 'first'); + emitter.off('foo', handler); + emitter.emit('foo', 'second'); + + expect(calls).toEqual(['first']); + }); + + test('once triggers listener only once', () => { + const emitter = new BrowserEventEmitter<{ foo: [string] }>(); + let callCount = 0; + + emitter.once('foo', () => { + callCount += 1; + }); + + emitter.emit('foo', 'first'); + emitter.emit('foo', 'second'); + + expect(callCount).toBe(1); + }); + + test('multiple identical listeners fire for each registration and are removed by off', () => { + const emitter = new BrowserEventEmitter<{ foo: [string] }>(); + const calls: string[] = []; + + const handler = (value: string) => { + calls.push(value); + }; + + emitter.on('foo', handler); + emitter.on('foo', handler); + + emitter.emit('foo', 'first'); + expect(calls).toEqual(['first', 'first']); + + emitter.off('foo', handler); + emitter.emit('foo', 'second'); + + expect(calls).toEqual(['first', 'first']); + }); +}); + +describe('randomUUID', () => { + test('uses native crypto.randomUUID when available', () => { + const mockUUID = '12345678-1234-1234-1234-123456789abc'; + const originalCrypto = global.crypto; + + Object.defineProperty(global, 'crypto', { + value: { randomUUID: vi.fn(() => mockUUID) }, + configurable: true, + }); + + const result = randomUUID(); + expect(result).toBe(mockUUID); + expect(global.crypto.randomUUID).toHaveBeenCalled(); + + Object.defineProperty(global, 'crypto', { + value: originalCrypto, + configurable: true, + }); + }); + + test('uses fallback when crypto.randomUUID is unavailable', () => { + const originalCrypto = global.crypto; + + Object.defineProperty(global, 'crypto', { + value: undefined, + configurable: true, + }); + + const result = randomUUID(); + expect(result).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); + + Object.defineProperty(global, 'crypto', { + value: originalCrypto, + configurable: true, + }); + }); + + test('fallback generates valid UUID v4 format', () => { + const originalCrypto = global.crypto; + + Object.defineProperty(global, 'crypto', { + value: undefined, + configurable: true, + }); + + const uuids = Array.from({ length: 10 }, () => randomUUID()); + + uuids.forEach((uuid) => { + expect(uuid).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/, + ); + }); + + const uniqueUUIDs = new Set(uuids); + expect(uniqueUUIDs.size).toBe(uuids.length); + + Object.defineProperty(global, 'crypto', { + value: originalCrypto, + configurable: true, + }); + }); +});