Skip to content

Commit e2e3abc

Browse files
authored
feat(node): crypto.randomUUID (#1697)
* feat(node): implement crypto.randomUUID() Adds Node.js-compatible crypto.randomUUID() implementation that returns RFC-4122 v4 UUID strings in lowercase format. - Add randomUUID() to CryptoAPI interface - Implement NodeCrypto module with ProxyExecutable exposure - Add tests covering changes (5 tests) for format, uniqueness and options - Uses java.util.UUID.randomUUID() for secure random generation --------- Signed-off-by: Gordon MacMillan <[email protected]>
1 parent 5f3bbf4 commit e2e3abc

File tree

4 files changed

+146
-5
lines changed

4 files changed

+146
-5
lines changed

packages/graalvm/api/graalvm.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3679,6 +3679,8 @@ public abstract interface class elide/runtime/intrinsics/js/node/ConsoleAPI : el
36793679
}
36803680

36813681
public abstract interface class elide/runtime/intrinsics/js/node/CryptoAPI : elide/runtime/intrinsics/js/node/NodeAPI {
3682+
public abstract fun randomUUID (Lorg/graalvm/polyglot/Value;)Ljava/lang/String;
3683+
public static synthetic fun randomUUID$default (Lelide/runtime/intrinsics/js/node/CryptoAPI;Lorg/graalvm/polyglot/Value;ILjava/lang/Object;)Ljava/lang/String;
36823684
}
36833685

36843686
public abstract interface class elide/runtime/intrinsics/js/node/DNSAPI : elide/runtime/intrinsics/js/node/NodeAPI {

packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/node/CryptoAPI.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Elide Technologies, Inc.
2+
* Copyright (c) 2024-2025 Elide Technologies, Inc.
33
*
44
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
55
* with the License. You may obtain a copy of the License at
@@ -12,9 +12,22 @@
1212
*/
1313
package elide.runtime.intrinsics.js.node
1414

15+
import org.graalvm.polyglot.Value
1516
import elide.annotations.API
17+
import elide.vm.annotations.Polyglot
1618

1719
/**
1820
* ## Node API: Crypto
1921
*/
20-
@API public interface CryptoAPI : NodeAPI
22+
@API public interface CryptoAPI : NodeAPI {
23+
/**
24+
* ## Crypto: randomUUID
25+
* Generates a random RFC 4122 version 4 UUID. The UUID is generated using a cryptographic pseudorandom number generator.
26+
*
27+
* See also: [Node Crypto API: `randomUUID`](https://nodejs.org/api/crypto.html#cryptorandomuuidoptions)
28+
*
29+
* @param options Optional settings (supports `disableEntropyCache` property, but is currently ignored)
30+
* @return A randomly generated 36 character UUID c4 string in lowercase format (e.g. "5cb34cef-5fc2-47e4-a3ac-4bb055fa2025")
31+
*/
32+
@Polyglot public fun randomUUID(options: Value? = null): String
33+
}

packages/graalvm/src/main/kotlin/elide/runtime/node/crypto/NodeCrypto.kt

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313
package elide.runtime.node.crypto
1414

15+
import org.graalvm.polyglot.Value
1516
import org.graalvm.polyglot.proxy.ProxyExecutable
1617
import elide.runtime.gvm.api.Intrinsic
1718
import elide.runtime.gvm.internals.intrinsics.js.AbstractNodeBuiltinModule
@@ -22,10 +23,14 @@ import elide.runtime.interop.ReadOnlyProxyObject
2223
import elide.runtime.intrinsics.GuestIntrinsic.MutableIntrinsicBindings
2324
import elide.runtime.intrinsics.js.node.CryptoAPI
2425
import elide.runtime.lang.javascript.NodeModuleName
26+
import elide.vm.annotations.Polyglot
2527

2628
// Internal symbol where the Node built-in module is installed.
2729
private const val CRYPTO_MODULE_SYMBOL = "node_${NodeModuleName.CRYPTO}"
2830

31+
// Functiopn name for randomUUID
32+
private const val F_RANDOM_UUID = "randomUUID"
33+
2934
// Installs the Node crypto module into the intrinsic bindings.
3035
@Intrinsic internal class NodeCryptoModule : AbstractNodeBuiltinModule() {
3136
private val singleton by lazy { NodeCrypto.create() }
@@ -45,10 +50,32 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
4550

4651
internal companion object {
4752
@JvmStatic fun create(): NodeCrypto = NodeCrypto()
53+
54+
// Module members
55+
private val moduleMembers = arrayOf(
56+
F_RANDOM_UUID,
57+
).apply { sort() }
58+
}
59+
60+
// Implement the CryptoAPI method
61+
@Polyglot override fun randomUUID(options: Value?): String{
62+
// Note `options` parameter exists for Node.js compatibility but is currently ignored
63+
// It supports { disableEntropyCache: boolean } which is not applicable to our implementation
64+
return java.util.UUID.randomUUID().toString()
4865
}
4966

50-
// @TODO not yet implemented
67+
// ProxyObject implementation
68+
override fun getMemberKeys(): Array<String> = moduleMembers
69+
70+
override fun hasMember(key: String): Boolean =
71+
moduleMembers.binarySearch(key) >= 0
5172

52-
override fun getMemberKeys(): Array<String> = emptyArray()
53-
override fun getMember(key: String?): Any? = null
73+
override fun getMember(key: String): Any? = when (key) {
74+
F_RANDOM_UUID -> ProxyExecutable { args ->
75+
// Node.js signature: randomUUID([options])
76+
val options = args.getOrNull(0)
77+
randomUUID(options)
78+
}
79+
else -> null
80+
}
5481
}

packages/graalvm/src/test/kotlin/elide/runtime/node/NodeCryptoTest.kt

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
package elide.runtime.node
1414

1515
import kotlin.test.Test
16+
import kotlin.test.assertEquals
17+
import kotlin.test.assertIs
18+
import kotlin.test.assertNotEquals
19+
import kotlin.test.assertTrue
1620
import kotlin.test.assertNotNull
1721
import elide.annotations.Inject
1822
import elide.runtime.node.crypto.NodeCryptoModule
@@ -99,4 +103,99 @@ import elide.testing.annotations.TestCase
99103
@Test override fun testInjectable() {
100104
assertNotNull(crypto)
101105
}
106+
107+
@Test fun `randomUUID should return a string`() = conforms {
108+
val uuid = crypto.provide().randomUUID(null)
109+
assertIs<String>(uuid, "randomUUID should return a String")
110+
}.guest {
111+
//language=javascript
112+
"""
113+
const crypto = require("crypto")
114+
const assert = require("assert")
115+
116+
const uuid = crypto.randomUUID();
117+
assert.equal(typeof uuid, "string");
118+
"""
119+
}
120+
121+
@Test fun `randomUUID should return lowercase format`() = conforms {
122+
val uuid = crypto.provide().randomUUID(null)
123+
assertEquals(uuid, uuid.lowercase(), "UUID should be in lowercase format")
124+
}.guest {
125+
//language=javascript
126+
"""
127+
const crypto = require("crypto")
128+
const assert = require("assert")
129+
130+
const uuid = crypto.randomUUID();
131+
assert.equal(uuid, uuid.toLowerCase(), "UUID should be lowercase");
132+
"""
133+
}
134+
135+
@Test fun `randomUUID should return valid UUID v4 format`() = conforms {
136+
val uuid = crypto.provide().randomUUID(null)
137+
138+
// UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
139+
// where y is one of [8,9,a,b]
140+
val uuidRegex = Regex("^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
141+
assertTrue(
142+
uuidRegex.matches(uuid),
143+
"UUID should match v4 format: $uuid"
144+
)
145+
assertEquals(36, uuid.length, "UUID should be 36 characters long")
146+
}.guest {
147+
//language=javascript
148+
"""
149+
const crypto = require("crypto")
150+
const assert = require("assert")
151+
152+
const uuid = crypto.randomUUID();
153+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
154+
155+
assert.equal(uuid.length, 36, "UUID should be 36 characters");
156+
assert.ok(uuidRegex.test(uuid), "UUID should match v4 format: " + uuid);
157+
"""
158+
}
159+
160+
@Test fun `randomUUID should generate unique values`() = conforms {
161+
val uuid1 = crypto.provide().randomUUID(null)
162+
val uuid2 = crypto.provide().randomUUID(null)
163+
val uuid3 = crypto.provide().randomUUID(null)
164+
165+
assertNotEquals(uuid1, uuid2, "UUIDS should be unique")
166+
assertNotEquals(uuid2, uuid3, "UUIDS should be unique")
167+
assertNotEquals(uuid1, uuid3, "UUIDS should be unique")
168+
}.guest {
169+
//language=javascript
170+
"""
171+
const crypto = require("crypto")
172+
const assert = require("assert")
173+
174+
const uuid1 = crypto.randomUUID();
175+
const uuid2 = crypto.randomUUID();
176+
const uuid3 = crypto.randomUUID();
177+
178+
assert.notEqual(uuid1, uuid2, "UUIDs should be unique");
179+
assert.notEqual(uuid2, uuid3, "UUIDs should be unique");
180+
assert.notEqual(uuid1, uuid3, "UUIDs should be unique");
181+
"""
182+
}
183+
184+
@Test fun `randomUUID should accept optional options parameter`() = conforms {
185+
// Options parameter is accepted but currently ignored
186+
val uuid = crypto.provide().randomUUID(null)
187+
assertNotNull(uuid)
188+
}.guest {
189+
//language=javascript
190+
"""
191+
const crypto = require("crypto")
192+
const assert = require("assert")
193+
194+
const uuid1 = crypto.randomUUID({ disableEntropyCache: true });
195+
const uuid2 = crypto.randomUUID({ disableEntropyCache: false });
196+
197+
assert.equal(typeof uuid1, "string");
198+
assert.equal(typeof uuid2, "string");
199+
"""
200+
}
102201
}

0 commit comments

Comments
 (0)