From a3f53f90e069eba09266e52b1f638aefb75716e7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 13 Aug 2026 14:46:50 +0000 Subject: [PATCH 1/3] fix(client): polyfill crypto.randomUUID for in-app browsers Remix frame reconcile calls crypto.randomUUID(); Twitter/X iOS WebViews can omit it and abort hydration (KODY-CLOUDFLARE-4P). Install a getRandomValues-based v4 polyfill before client boot. Co-authored-by: Kent C. Dodds --- .../ensure-crypto-random-uuid.node.test.ts | 40 ++++++++++++++++++ .../client/ensure-crypto-random-uuid.ts | 41 +++++++++++++++++++ packages/worker/client/entry.tsx | 3 ++ 3 files changed, 84 insertions(+) create mode 100644 packages/worker/client/ensure-crypto-random-uuid.node.test.ts create mode 100644 packages/worker/client/ensure-crypto-random-uuid.ts diff --git a/packages/worker/client/ensure-crypto-random-uuid.node.test.ts b/packages/worker/client/ensure-crypto-random-uuid.node.test.ts new file mode 100644 index 0000000000..da0882c1bd --- /dev/null +++ b/packages/worker/client/ensure-crypto-random-uuid.node.test.ts @@ -0,0 +1,40 @@ +import { expect, test } from 'vitest' +import { ensureCryptoRandomUUID } from './ensure-crypto-random-uuid.ts' + +const uuidPattern = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + +test('ensureCryptoRandomUUID polyfills missing randomUUID from getRandomValues and leaves an existing implementation alone', () => { + const bytes = new Uint8Array(16).fill(0xab) + const getRandomValues = (target: Uint8Array) => { + target.set(bytes.subarray(0, target.length)) + return target + } + const cryptoWithoutUuid = { + getRandomValues, + } as Crypto + + ensureCryptoRandomUUID(cryptoWithoutUuid) + + expect(typeof cryptoWithoutUuid.randomUUID).toBe('function') + const first = cryptoWithoutUuid.randomUUID() + expect(first).toMatch(uuidPattern) + expect(first[14]).toBe('4') + expect(['8', '9', 'a', 'b']).toContain(first[19]!.toLowerCase()) + + const existing = () => 'already-present-uuid' + const cryptoWithUuid = { + getRandomValues, + randomUUID: existing, + } as Crypto + + ensureCryptoRandomUUID(cryptoWithUuid) + expect(cryptoWithUuid.randomUUID).toBe(existing) + expect(cryptoWithUuid.randomUUID()).toBe('already-present-uuid') + + const empty = {} as Crypto + ensureCryptoRandomUUID(empty) + expect(empty.randomUUID).toBeUndefined() + + ensureCryptoRandomUUID(undefined) +}) diff --git a/packages/worker/client/ensure-crypto-random-uuid.ts b/packages/worker/client/ensure-crypto-random-uuid.ts new file mode 100644 index 0000000000..08365ea669 --- /dev/null +++ b/packages/worker/client/ensure-crypto-random-uuid.ts @@ -0,0 +1,41 @@ +/** + * Remix's reconcile runtime calls `crypto.randomUUID()` for frame ids. + * Some in-app browsers (e.g. Twitter/X on iOS) expose `crypto` / + * `getRandomValues` but not `randomUUID`, which aborts client hydration. + * Install a RFC 4122 v4 polyfill before `run()` when needed. + */ +export function ensureCryptoRandomUUID( + cryptoApi: Crypto | undefined = globalThis.crypto, +): void { + if (!cryptoApi) return + if (typeof cryptoApi.randomUUID === 'function') return + if (typeof cryptoApi.getRandomValues !== 'function') return + + const polyfill = function randomUUID(this: Crypto): string { + const bytes = new Uint8Array(16) + this.getRandomValues(bytes) + // Version 4 + RFC 4122 variant bits. + 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)}` + } + + try { + Object.defineProperty(cryptoApi, 'randomUUID', { + configurable: true, + enumerable: false, + writable: true, + value: polyfill, + }) + } catch { + try { + ;(cryptoApi as Crypto & { randomUUID: typeof polyfill }).randomUUID = + polyfill + } catch { + // Leave crypto unchanged; Remix will still throw if it calls randomUUID. + } + } +} diff --git a/packages/worker/client/entry.tsx b/packages/worker/client/entry.tsx index 67a44aa161..c37c26c47b 100644 --- a/packages/worker/client/entry.tsx +++ b/packages/worker/client/entry.tsx @@ -7,7 +7,10 @@ import { initSentryClient, } from '#client/sentry-client.ts' import { AppRoot, APP_ROOT_ENTRY_ID } from './app-root.tsx' +import { ensureCryptoRandomUUID } from './ensure-crypto-random-uuid.ts' +// Remix frame ids call crypto.randomUUID(); some in-app browsers omit it. +ensureCryptoRandomUUID() initSentryClient(document) const clientRegistry: Record = { From 48d481f480e2b3c2f9f3c6705083d40e540db9db Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 13 Aug 2026 14:56:16 +0000 Subject: [PATCH 2/3] fix(client): satisfy Crypto.randomUUID return type Cast the polyfill UUID string to the DOM lib template-literal return type so worker typecheck passes. Co-authored-by: Kent C. Dodds --- packages/worker/client/ensure-crypto-random-uuid.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/worker/client/ensure-crypto-random-uuid.ts b/packages/worker/client/ensure-crypto-random-uuid.ts index 08365ea669..bd5367e62e 100644 --- a/packages/worker/client/ensure-crypto-random-uuid.ts +++ b/packages/worker/client/ensure-crypto-random-uuid.ts @@ -11,7 +11,9 @@ export function ensureCryptoRandomUUID( if (typeof cryptoApi.randomUUID === 'function') return if (typeof cryptoApi.getRandomValues !== 'function') return - const polyfill = function randomUUID(this: Crypto): string { +const polyfill = function randomUUID( + this: Crypto, + ): `${string}-${string}-${string}-${string}-${string}` { const bytes = new Uint8Array(16) this.getRandomValues(bytes) // Version 4 + RFC 4122 variant bits. @@ -20,7 +22,7 @@ export function ensureCryptoRandomUUID( 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)}` + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` as `${string}-${string}-${string}-${string}-${string}` } try { @@ -32,8 +34,7 @@ export function ensureCryptoRandomUUID( }) } catch { try { - ;(cryptoApi as Crypto & { randomUUID: typeof polyfill }).randomUUID = - polyfill + cryptoApi.randomUUID = polyfill } catch { // Leave crypto unchanged; Remix will still throw if it calls randomUUID. } From 568d47d5fb0b0c24d3eb36d572878f1619d37a69 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 13 Aug 2026 15:03:56 +0000 Subject: [PATCH 3/3] style(client): fix indentation in randomUUID polyfill oxfmt check failed on the polyfill helper after the return-type cast edit. Co-authored-by: Kent C. Dodds --- packages/worker/client/ensure-crypto-random-uuid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/worker/client/ensure-crypto-random-uuid.ts b/packages/worker/client/ensure-crypto-random-uuid.ts index bd5367e62e..42079d290f 100644 --- a/packages/worker/client/ensure-crypto-random-uuid.ts +++ b/packages/worker/client/ensure-crypto-random-uuid.ts @@ -11,7 +11,7 @@ export function ensureCryptoRandomUUID( if (typeof cryptoApi.randomUUID === 'function') return if (typeof cryptoApi.getRandomValues !== 'function') return -const polyfill = function randomUUID( + const polyfill = function randomUUID( this: Crypto, ): `${string}-${string}-${string}-${string}-${string}` { const bytes = new Uint8Array(16)