Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c12e540
fix(Runtime): add 25.0.1 to version map and missing netty runtime deps.
Oct 30, 2025
9f1835b
feat(randomInt): add randomInt method.
Oct 30, 2025
c1742be
chore(randomInt): update type signatures and executable.
Oct 31, 2025
8a55960
chore(Crypto): add randomInt callback type signature.
Nov 6, 2025
69aeb36
chore(randomInt): update type signatures and refactor initial impleme…
Nov 8, 2025
a79d02d
refactor(CryptoIntrinsics): update randomInt placeholder signature.
Nov 8, 2025
904b44b
chore(CryptoOptions): add package declaration.
Nov 9, 2025
df2a668
chore(WebCrypto): update method return type, add suggested implementa…
Nov 9, 2025
16cf5af
chore(CryptoAPi): update randomIntCallback import path, remove unused…
Nov 9, 2025
864be07
docs(CryptoAPI): update return comment to include void like value if …
Nov 9, 2025
f2e0ebf
refactor(NodeCrypto): ensure only 1 instance of SecureRandom is gener…
Nov 9, 2025
514bebb
chore(RandomInt): begin adding test cases for method implementation.
Nov 9, 2025
ff70d32
test(RandomInt): add more test cases.
Nov 11, 2025
eae9ffb
refactor(RandomInt): update signature and remove unused method.
Nov 11, 2025
01ffd44
docs(RandomInt): add comments describing the remaining randomInt meth…
Nov 11, 2025
18659d4
chore(randomInt): update method and related tests.
ElijahKotyluk Nov 20, 2025
e28cd4d
test(randomInt): write more tests, address issues with method.
ElijahKotyluk Nov 20, 2025
d2ee56b
chore(randomInt): remove unused imports from test file, add additiona…
ElijahKotyluk Nov 21, 2025
2da4236
refactor(randomInt): rewrite implementation and tests.
ElijahKotyluk Nov 24, 2025
64abe63
chore(RandomInt): clean up implementation, comments, and tests.
ElijahKotyluk Nov 24, 2025
797624d
refactor(randomInt): reduce repetitive code, update calls in testing,…
ElijahKotyluk Nov 25, 2025
f22d3bc
refactor(randomInt): remove unnecessary overload, clean up comments a…
ElijahKotyluk Nov 25, 2025
efe3f4e
chore(api): updated file.
ElijahKotyluk Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/graalvm/api/graalvm.api
Original file line number Diff line number Diff line change
Expand Up @@ -4366,6 +4366,11 @@ public abstract interface class elide/runtime/intrinsics/js/node/ConsoleAPI : el
}

public abstract interface class elide/runtime/intrinsics/js/node/CryptoAPI : elide/runtime/intrinsics/js/node/NodeAPI {
public abstract fun randomInt (JJ)J
public abstract fun randomInt (JJLkotlin/jvm/functions/Function2;)V
public abstract fun randomInt (Lorg/graalvm/polyglot/Value;)J
public abstract fun randomInt (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)J
public abstract fun randomInt (Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;Lorg/graalvm/polyglot/Value;)V
public abstract fun randomUUID (Lorg/graalvm/polyglot/Value;)Ljava/lang/String;
public static synthetic fun randomUUID$default (Lelide/runtime/intrinsics/js/node/CryptoAPI;Lorg/graalvm/polyglot/Value;ILjava/lang/Object;)Ljava/lang/String;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import elide.runtime.gvm.internals.intrinsics.js.typed.UUIDValue
import elide.runtime.intrinsics.GuestIntrinsic
import elide.runtime.intrinsics.js.Crypto.Companion.MAX_RANDOM_BYTES_SIZE
import elide.runtime.intrinsics.js.SubtleCrypto
import elide.runtime.intrinsics.js.err.AbstractJsException
import elide.runtime.intrinsics.js.err.QuotaExceededError
import elide.runtime.intrinsics.js.err.ValueError
import elide.runtime.intrinsics.js.typed.UUID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package elide.runtime.intrinsics.js.node

import org.graalvm.polyglot.Value
import elide.annotations.API
import elide.runtime.intrinsics.js.node.crypto.RandomIntCallback
import elide.vm.annotations.Polyglot

/**
Expand All @@ -30,4 +31,56 @@ import elide.vm.annotations.Polyglot
* @return A randomly generated 36 character UUID c4 string in lowercase format (e.g. "5cb34cef-5fc2-47e4-a3ac-4bb055fa2025")
*/
@Polyglot public fun randomUUID(options: Value? = null): String

/**
* ## Crypto: `randomInt`
* Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
*
* See also: [Node Crypto API: `randomInt`](https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback)
*
* @param min Lower bound (inclusive)
* @param max Upper bound (exclusive)
* @param callback Callback to receive the generated safe integer or an error.
* @return Unit
*/
@Polyglot public fun randomInt(min: Long, max: Long, callback: RandomIntCallback)

/**
* ## Crypto: randomInt
* Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
*
* See also: [Node Crypto API: `randomInt`](https://nodejs.org/api/crypto.html#cryptorandomintmin-max-callback)
*
* @param min Lower bound (inclusive)
* @param max Upper bound (exclusive)
* @return A randomly generated safe integer between `min` (inclusive) and `max` (exclusive).
*/
@Polyglot public fun randomInt(min: Long, max: Long): Long

/**
* ## Crypto: randomInt
* Public overload of [randomInt].
* Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
* @param min Lower bound (inclusive)
* @param max Upper bound (exclusive)
* @param callback Callback to receive the generated safe integer or an error.
*/
@Polyglot public fun randomInt(min: Value, max: Value, callback: Value)

/**
* ## Crypto: randomInt
* Public overload of [randomInt].
* Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
* @param min Lower bound (inclusive)
* @param max Upper bound (exclusive)
*/
@Polyglot public fun randomInt(min: Value, max: Value): Long

/**
* ## Crypto: randomInt
* Public overload of [randomInt].
* Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
* @param max Upper bound (exclusive)
*/
@Polyglot public fun randomInt(max: Value): Long
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2024-2025 Elide Technologies, Inc.
*
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://opensource.org/license/mit/
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under the License.
*/
package elide.runtime.intrinsics.js.node.crypto

/**
* ## Callback: `crypto.randomInt`
*
* Describes the callback function shape which is provided to the `randomInt` operation.
*/
public typealias RandomIntCallback = (err: Throwable?, value: Long?) -> Unit
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ package elide.runtime.node.crypto

import org.graalvm.polyglot.Value
import org.graalvm.polyglot.proxy.ProxyExecutable
import java.math.BigInteger
import elide.runtime.gvm.api.Intrinsic
import elide.runtime.gvm.internals.intrinsics.js.AbstractNodeBuiltinModule
import elide.runtime.gvm.js.JsError
import elide.runtime.gvm.js.JsSymbol.JsSymbols.asJsSymbol
import elide.runtime.gvm.loader.ModuleInfo
import elide.runtime.gvm.loader.ModuleRegistry
Expand All @@ -24,12 +26,85 @@ import elide.runtime.intrinsics.GuestIntrinsic.MutableIntrinsicBindings
import elide.runtime.intrinsics.js.node.CryptoAPI
import elide.runtime.lang.javascript.NodeModuleName
import elide.vm.annotations.Polyglot
import java.security.SecureRandom
import kotlin.concurrent.thread
import elide.runtime.intrinsics.js.err.RangeError
import elide.runtime.intrinsics.js.err.TypeError
import elide.runtime.intrinsics.js.node.crypto.RandomIntCallback

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

// Functiopn name for randomUUID
// Function name for randomUUID
private const val F_RANDOM_UUID = "randomUUID"
private const val F_RANDOM_INT = "randomInt"

// Cached Int generator to ensure we don't create multiple instances.
private val cryptoRandomGenerator by lazy { SecureRandom() }

// The maximum range (max - min) allowed is 2^48 in Node.js.
private val MAX_48_BIT_LIMIT = BigInteger.valueOf(2L).pow(48)

// Generates a cryptographically secure random integer between the specified `min` (inclusive) and `max` (exclusive) values.
private fun genRandomInt(min: Long, max: Long): Long {
try {
return cryptoRandomGenerator.nextLong(min, max)
} catch (e: Throwable) {
throw TypeError.create("Error generating random bytes for randomInt: ${e.message}")
}
}

// Safely converts a Value to a BigInteger, ensuring it is a safe integer within JS limits.
private fun safeValueToBigInt(value: Value, name: String): BigInteger {
if (value.isNumber) {
val bigIntValue: BigInteger? = when {
value.fitsInLong() -> {
BigInteger.valueOf(value.asLong())
}
// Reject integers that exceed Long.MAX_VALUE or are less than Long.MIN_VALUE
value.fitsInBigInteger() -> throw RangeError.create("The \"$name\" argument must be a safe integer. Received an integer that exceeds the max bounds ${MAX_48_BIT_LIMIT}.")
// Reject non-integer numbers
value.fitsInDouble() -> {
throw TypeError.create("The \"$name\" argument must be a safe integer. Received a non-integer number: ${value.asDouble()}.")
}
else -> null // Reject non-integer (e.g. Infinity, NaN, very large BigInts)
}

// Define JS safe integer bounds
val jsMaxSafeInt = BigInteger("9007199254740991") // 2^53 - 1
val jsMinSafeInt = BigInteger("-9007199254740991") // -(2^53 - 1)

// Final check: even if conversion works, ensure it falls within JS safe limits
if (bigIntValue != null && bigIntValue >= jsMinSafeInt && bigIntValue <= jsMaxSafeInt) {
return bigIntValue
}
}
// Invalid value type, we don't want it
throw TypeError.create("The \"$name\" argument must be a safe integer. Received ${value}.")
}

// Validates that the provided min and max values are safe integers and that the range difference does not exceed 2^48.
private fun genSafeRange(min: Value, max: Value): Pair<Long, Long> {
// Safely convert both inputs to BigInteger
val minBigInt = safeValueToBigInt(min, "min")
val maxBigInt = safeValueToBigInt(max, "max")

// Enforce the Min <= Max rule otherwise we throw a RangeError
if (minBigInt >= maxBigInt) {
throw RangeError.create("The value of \"max\" is out of range. It must be greater than the value of \"min\" (${minBigInt}). Received ${maxBigInt}.")
}

val rangeDifference = maxBigInt.subtract(minBigInt)

// If the range difference exceeds 2^48, we throw a RangeError. Node.js has a range limit of 2^48 for randomInt.
if (rangeDifference > MAX_48_BIT_LIMIT) {
println("Range difference exceeds 2^48 limit: $rangeDifference")
throw RangeError.create("The value of \"max - min\" is out of range. It must be <= 281474976710655. Received ${rangeDifference}.")
}

// Return the validated safe Long values
return Pair(minBigInt.toLong(), maxBigInt.toLong())
}

// Installs the Node crypto module into the intrinsic bindings.
@Intrinsic internal class NodeCryptoModule : AbstractNodeBuiltinModule() {
Expand All @@ -46,14 +121,13 @@ private const val F_RANDOM_UUID = "randomUUID"
* # Node API: `crypto`
*/
internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAPI {
//

internal companion object {
@JvmStatic fun create(): NodeCrypto = NodeCrypto()

// Module members
private val moduleMembers = arrayOf(
F_RANDOM_UUID,
F_RANDOM_INT,
).apply { sort() }
}

Expand All @@ -64,10 +138,51 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
return java.util.UUID.randomUUID().toString()
}

@Polyglot override fun randomInt(min: Long, max: Long): Long {
return genRandomInt(min, max)
}

@Polyglot override fun randomInt(min: Long, max: Long, callback: RandomIntCallback) {
val randomValue = genRandomInt(min, max)

thread {
try {
callback.invoke(null, randomValue)
} catch (e: Throwable) {
callback.invoke(TypeError.create(e.message ?: "Unknown error"), randomValue)
}
}
}

@Polyglot override fun randomInt(min: Value, max: Value, callback: Value) {
val (safeMin, safeMax) = genSafeRange(min, max)

val safeCallback: RandomIntCallback = callback.let { cb ->
{ err: Throwable?, value: Long? ->
cb.execute(
err?.let { Value.asValue(it) },
value?.let { Value.asValue(it) }
)
}
}

return randomInt(safeMin, safeMax, safeCallback)
}

@Polyglot override fun randomInt(min: Value, max: Value): Long {
val (safeMin, safeMax) = genSafeRange(min, max)
return randomInt(safeMin, safeMax)
}

@Polyglot override fun randomInt(max: Value): Long {
val (safeMin, safeMax) = genSafeRange(Value.asValue(0), max)
return randomInt(safeMin, safeMax)
}

// ProxyObject implementation
override fun getMemberKeys(): Array<String> = moduleMembers

override fun hasMember(key: String): Boolean =
override fun hasMember(key: String): Boolean =
moduleMembers.binarySearch(key) >= 0

override fun getMember(key: String): Any? = when (key) {
Expand All @@ -76,6 +191,31 @@ internal class NodeCrypto private constructor () : ReadOnlyProxyObject, CryptoAP
val options = args.getOrNull(0)
randomUUID(options)
}
F_RANDOM_INT -> ProxyExecutable { args ->
// Check if last argument is a callback function
val lastIsCb = args.lastOrNull()?.canExecute() == true

when (args.size) {
1 -> {
// randomInt(max)
this.randomInt(args[0])
}
2 -> {
if (lastIsCb) {
// randomInt(max, callback)
this.randomInt(Value.asValue(0), args[0], args.last())
} else {
// randomInt(min, max)
this.randomInt(args[0], args[1])
}
}
3 -> {
// randomInt(min, max, callback)
this.randomInt(args[0], args[1], args.last())
}
else -> throw JsError.typeError("Invalid number of arguments for crypto.randomInt: ${args.size}")
}
}
else -> null
}
}
Loading
Loading