Skip to content

Commit 0658b5e

Browse files
authored
Add randomUUID to CryptoProvider for edge runtime compatibility (#1404)
## Summary Fixes #1403 - Adds `randomUUID()` abstract method to `CryptoProvider` interface - Implements in `NodeCryptoProvider` using Node's `crypto.randomUUID()` - Implements in `SubtleCryptoProvider` using `randomBytes(16)` with UUID v4 bit manipulation - Updates `AuditLogs` to use `getCryptoProvider().randomUUID()` instead of direct Node crypto import This ensures the audit-logs idempotency key generation works across all supported runtimes including edge environments (Convex, Cloudflare Workers) that cannot import the Node "crypto" package.
1 parent b8ee00c commit 0658b5e

File tree

5 files changed

+84
-2
lines changed

5 files changed

+84
-2
lines changed

src/audit-logs/audit-logs.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { randomUUID } from 'crypto';
21
import { WorkOS } from '../workos';
32
import {
43
CreateAuditLogEventOptions,
@@ -34,7 +33,9 @@ export class AuditLogs {
3433
// Auto-generate idempotency key if not provided
3534
const optionsWithIdempotency: CreateAuditLogEventRequestOptions = {
3635
...options,
37-
idempotencyKey: options.idempotencyKey || `workos-node-${randomUUID()}`,
36+
idempotencyKey:
37+
options.idempotencyKey ||
38+
`workos-node-${this.workos.getCryptoProvider().randomUUID()}`,
3839
};
3940

4041
await this.workos.post(

src/common/crypto/crypto-provider.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,52 @@ describe('CryptoProvider', () => {
6565
);
6666
});
6767
});
68+
69+
describe('when generating UUIDs', () => {
70+
const UUID_V4_REGEX =
71+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
72+
73+
it('generates valid UUID v4 format for NodeCryptoProvider', () => {
74+
const nodeCryptoProvider = new NodeCryptoProvider();
75+
const uuid = nodeCryptoProvider.randomUUID();
76+
77+
expect(uuid).toMatch(UUID_V4_REGEX);
78+
});
79+
80+
it('generates valid UUID v4 format for SubtleCryptoProvider', () => {
81+
const subtleCryptoProvider = new SubtleCryptoProvider();
82+
const uuid = subtleCryptoProvider.randomUUID();
83+
84+
expect(uuid).toMatch(UUID_V4_REGEX);
85+
});
86+
87+
it('generates unique UUIDs', () => {
88+
const nodeCryptoProvider = new NodeCryptoProvider();
89+
const subtleCryptoProvider = new SubtleCryptoProvider();
90+
91+
const uuids = new Set([
92+
nodeCryptoProvider.randomUUID(),
93+
nodeCryptoProvider.randomUUID(),
94+
subtleCryptoProvider.randomUUID(),
95+
subtleCryptoProvider.randomUUID(),
96+
]);
97+
98+
expect(uuids.size).toBe(4);
99+
});
100+
101+
it('SubtleCryptoProvider falls back when crypto.randomUUID is unavailable', () => {
102+
const originalRandomUUID = globalThis.crypto.randomUUID;
103+
// @ts-ignore - intentionally removing for test
104+
delete globalThis.crypto.randomUUID;
105+
106+
try {
107+
const subtleCryptoProvider = new SubtleCryptoProvider();
108+
const uuid = subtleCryptoProvider.randomUUID();
109+
110+
expect(uuid).toMatch(UUID_V4_REGEX);
111+
} finally {
112+
globalThis.crypto.randomUUID = originalRandomUUID;
113+
}
114+
});
115+
});
68116
});

src/common/crypto/crypto-provider.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,11 @@ export abstract class CryptoProvider {
8282
* @returns A Uint8Array containing the random bytes
8383
*/
8484
abstract randomBytes(length: number): Uint8Array;
85+
86+
/**
87+
* Generates a random UUID v4 string.
88+
*
89+
* @returns A UUID v4 string in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
90+
*/
91+
abstract randomUUID(): string;
8592
}

src/common/crypto/node-crypto-provider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,8 @@ export class NodeCryptoProvider extends CryptoProvider {
9696
randomBytes(length: number): Uint8Array {
9797
return new Uint8Array(crypto.randomBytes(length));
9898
}
99+
100+
randomUUID(): string {
101+
return crypto.randomUUID();
102+
}
99103
}

src/common/crypto/subtle-crypto-provider.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,28 @@ export class SubtleCryptoProvider extends CryptoProvider {
173173
crypto.getRandomValues(bytes);
174174
return bytes;
175175
}
176+
177+
randomUUID(): string {
178+
if (
179+
typeof crypto !== 'undefined' &&
180+
typeof crypto.randomUUID === 'function'
181+
) {
182+
return crypto.randomUUID();
183+
}
184+
185+
// Fallback for environments without crypto.randomUUID
186+
const bytes = this.randomBytes(16);
187+
// tslint:disable-next-line:no-bitwise
188+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
189+
// tslint:disable-next-line:no-bitwise
190+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant
191+
192+
const hex = Array.from(bytes, (b) => byteHexMapping[b]).join('');
193+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(
194+
12,
195+
16,
196+
)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
197+
}
176198
}
177199

178200
// Cached mapping of byte to hex representation. We do this once to avoid re-

0 commit comments

Comments
 (0)