diff --git a/crates/bindings-typescript/package.json b/crates/bindings-typescript/package.json index e2b50cb78dc..0d605a2bc3e 100644 --- a/crates/bindings-typescript/package.json +++ b/crates/bindings-typescript/package.json @@ -162,6 +162,7 @@ "fast-text-encoding": "^1.0.0", "headers-polyfill": "^4.0.3", "prettier": "^3.3.3", + "pure-rand": "^7.0.1", "statuses": "^2.0.2", "url-polyfill": "^1.1.14" }, diff --git a/crates/bindings-typescript/src/lib/reducers.ts b/crates/bindings-typescript/src/lib/reducers.ts index 24e3459cbc4..50dc8ee9354 100644 --- a/crates/bindings-typescript/src/lib/reducers.ts +++ b/crates/bindings-typescript/src/lib/reducers.ts @@ -24,6 +24,7 @@ import type { ReducerSchema } from './reducer_schema'; import { toCamelCase, toPascalCase } from './util'; import type { CamelCase } from './type_util'; import { Uuid } from './uuid.ts'; +import type { Random } from '../server/rng'; /** * Helper to extract the parameter types from an object type @@ -123,6 +124,7 @@ export type ReducerCtx = Readonly<{ senderAuth: AuthCtx; newUuidV4(): Uuid; newUuidV7(): Uuid; + random: Random; }>; /** diff --git a/crates/bindings-typescript/src/server/index.ts b/crates/bindings-typescript/src/server/index.ts index 61df4bb591c..6d519749af6 100644 --- a/crates/bindings-typescript/src/server/index.ts +++ b/crates/bindings-typescript/src/server/index.ts @@ -9,6 +9,7 @@ export * from './query'; export type { ProcedureCtx, TransactionCtx } from '../lib/procedures'; export { toCamelCase } from '../lib/util'; export { type Uuid } from '../lib/uuid'; +export { type Random } from './rng'; import './polyfills'; // Ensure polyfills are loaded import './register_hooks'; // Ensure module hooks are registered diff --git a/crates/bindings-typescript/src/server/rng.ts b/crates/bindings-typescript/src/server/rng.ts new file mode 100644 index 00000000000..65a4758dcd9 --- /dev/null +++ b/crates/bindings-typescript/src/server/rng.ts @@ -0,0 +1,119 @@ +import type { RandomGenerator } from 'pure-rand'; +import { unsafeUniformBigIntDistribution } from 'pure-rand/distribution/UnsafeUniformBigIntDistribution'; +import { unsafeUniformIntDistribution } from 'pure-rand/distribution/UnsafeUniformIntDistribution'; +import { xoroshiro128plus } from 'pure-rand/generator/XoroShiro'; +import type { Timestamp } from '../lib/timestamp'; + +declare global { + interface Math { + random(): never; + } +} + +type IntArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array; + +/** + * A collection of random-number-generating functions, seeded based on `ctx.timestamp`. + * + * ## Usage + * + * ``` + * const floatOneToTen = ctx.random() * 10; + * const randomBytes = ctx.random.fill(new Uint8Array(16)); + * const intOneToTen = ctx.random.integerInRange(0, 10); + * ``` + */ +export interface Random { + /** + * Returns a random floating-point number in the range `[0.0, 1.0)`. + * + * The returned float will have 53 bits of randomness. + */ + (): number; + + /** + * Like `crypto.getRandomValues()`. Fills a `TypedArray` with random integers + * in a uniform distribution, mutating and returning it. + */ + fill(array: T): T; + + /** + * Returns a random unsigned 32-bit integer in a uniform distribution in the + * range `[0, 2**32)`. + */ + uint32(): number; + + /** + * Returns an integer in the range `[min, max]`. + */ + integerInRange(min: number, max: number): number; + + /** + * Returns a bigint in the range `[min, max]`. + */ + bigintInRange(min: bigint, max: bigint): bigint; +} + +const { asUintN } = BigInt; + +/** Based on the function of the same name in `rand_core::SeedableRng::seed_from_u64` */ +function pcg32(state: bigint): number { + const MUL = 6364136223846793005n; + const INC = 11634580027462260723n; + + state = asUintN(64, state * MUL + INC); + const xorshifted = Number(asUintN(32, ((state >> 18n) ^ state) >> 27n)); + const rot = Number(asUintN(32, state >> 59n)); + // rotate `xorshifted` right by `rot` bits + return (xorshifted >> rot) | (xorshifted << (32 - rot)); +} + +/** From the `pure-rand` README */ +function generateFloat64(rng: RandomGenerator): number { + const g1 = unsafeUniformIntDistribution(0, (1 << 26) - 1, rng); + const g2 = unsafeUniformIntDistribution(0, (1 << 27) - 1, rng); + const value = (g1 * Math.pow(2, 27) + g2) * Math.pow(2, -53); + return value; +} + +export function makeRandom(seed: Timestamp): Random { + // Use PCG32 to turn a 64-bit seed into a 32-bit seed, as the Rust `rand` crate does. + const rng = xoroshiro128plus(pcg32(seed.microsSinceUnixEpoch)); + + const random: Random = () => generateFloat64(rng); + + random.fill = array => { + const elem = array.at(0); + if (typeof elem === 'bigint') { + const upper = (1n << BigInt(array.BYTES_PER_ELEMENT * 8)) - 1n; + for (let i = 0; i < array.length; i++) { + array[i] = unsafeUniformBigIntDistribution(0n, upper, rng); + } + } else if (typeof elem === 'number') { + const upper = (1 << (array.BYTES_PER_ELEMENT * 8)) - 1; + for (let i = 0; i < array.length; i++) { + array[i] = unsafeUniformIntDistribution(0, upper, rng); + } + } + return array; + }; + + random.uint32 = () => rng.unsafeNext(); + + random.integerInRange = (min, max) => + unsafeUniformIntDistribution(min, max, rng); + + random.bigintInRange = (min, max) => + unsafeUniformBigIntDistribution(min, max, rng); + + return random; +} diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index 4d426d2b527..0f79ba43400 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -48,6 +48,7 @@ import type { DbView } from './db_view'; import { SenderError, SpacetimeHostError } from './errors'; import { Range, type Bound } from './range'; import ViewResultHeader from '../lib/autogen/view_result_header_type'; +import { makeRandom, type Random } from './rng'; const { freeze } = Object; @@ -195,6 +196,7 @@ export const ReducerCtxImpl = class ReducerCtx< #identity: Identity | undefined; #senderAuth: AuthCtx | undefined; #uuidCounter: { value: number } | undefined; + #random: Random | undefined; sender: Identity; timestamp: Timestamp; connectionId: ConnectionId | null; @@ -223,26 +225,25 @@ export const ReducerCtxImpl = class ReducerCtx< )); } + get random() { + return (this.#random ??= makeRandom(this.timestamp)); + } + /** - * Create a new random {@link Uuid} `v4` using the {@link crypto} RNG. - * - * WARN: Until we use a spacetime RNG this make calls non-deterministic. + * Create a new random {@link Uuid} `v4` using this `ReducerCtx`'s RNG. */ newUuidV4(): Uuid { // TODO: Use a spacetime RNG when available - const bytes = crypto.getRandomValues(new Uint8Array(16)); + const bytes = this.random.fill(new Uint8Array(16)); return Uuid.fromRandomBytesV4(bytes); } /** - * Create a new sortable {@link Uuid} `v7` using the {@link crypto} RNG, counter, - * and the timestamp. - * - * WARN: Until we use a spacetime RNG this make calls non-deterministic. + * Create a new sortable {@link Uuid} `v7` using this `ReducerCtx`'s RNG, counter, + * and timestamp. */ newUuidV7(): Uuid { - // TODO: Use a spacetime RNG when available - const bytes = crypto.getRandomValues(new Uint8Array(4)); + const bytes = this.random.fill(new Uint8Array(4)); const counter = (this.#uuidCounter ??= { value: 0 }); return Uuid.fromCounterV7(counter, this.timestamp, bytes); } diff --git a/crates/bindings-typescript/tsup.config.ts b/crates/bindings-typescript/tsup.config.ts index dc277bff62e..33fa0be825f 100644 --- a/crates/bindings-typescript/tsup.config.ts +++ b/crates/bindings-typescript/tsup.config.ts @@ -163,7 +163,7 @@ export default defineConfig([ ], }, external: ['undici', /^spacetime:sys.*$/], - noExternal: ['base64-js', 'fast-text-encoding', 'statuses'], + noExternal: ['base64-js', 'fast-text-encoding', 'statuses', 'pure-rand'], outExtension, esbuildOptions: commonEsbuildTweaks(), }, diff --git a/crates/core/src/host/v8/builtins/delete_math_random.js b/crates/core/src/host/v8/builtins/delete_math_random.js new file mode 100644 index 00000000000..689e0902a23 --- /dev/null +++ b/crates/core/src/host/v8/builtins/delete_math_random.js @@ -0,0 +1,10 @@ +delete Math.random; +Object.defineProperty(Math, 'random', { + enumerable: false, + configurable: true, + get() { + throw new TypeError( + 'Math.random is not available in SpacetimeDB modules. Use ctx.random instead.' + ); + }, +}); diff --git a/crates/core/src/host/v8/builtins/mod.rs b/crates/core/src/host/v8/builtins/mod.rs index d790415f388..180ec771c7e 100644 --- a/crates/core/src/host/v8/builtins/mod.rs +++ b/crates/core/src/host/v8/builtins/mod.rs @@ -16,6 +16,7 @@ pub(super) fn evalute_builtins(scope: &mut PinScope<'_, '_>) -> ExcResult<()> { }; } eval_builtin!("text_encoding.js")?; + eval_builtin!("delete_math_random.js")?; Ok(()) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 353e152481d..8346710cd07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: prettier: specifier: ^3.3.3 version: 3.6.2 + pure-rand: + specifier: ^7.0.1 + version: 7.0.1 react: specifier: ^18.0.0 || ^19.0.0-0 || ^19.0.0 version: 19.2.0 @@ -165,7 +168,7 @@ importers: version: 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-content-docs': specifier: 3.9.2 - version: 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/preset-classic': specifier: 3.9.2 version: 3.9.2(@algolia/client-search@5.39.0)(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.6.3) @@ -7495,6 +7498,9 @@ packages: resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} engines: {node: '>=12.20'} + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -10647,7 +10653,7 @@ snapshots: '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -10684,6 +10690,46 @@ snapshots: - webpack-cli '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': + dependencies: + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/logger': 3.9.2 + '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/react-router-config': 5.0.11 + combine-promises: 1.2.0 + fs-extra: 11.3.2 + js-yaml: 4.1.0 + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + schema-dts: 1.1.5 + tslib: 2.8.1 + utility-types: 3.11.0 + webpack: 5.102.0 + transitivePeerDependencies: + - '@docusaurus/faster' + - '@mdx-js/react' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@swc/css' + - bufferutil + - csso + - debug + - esbuild + - lightningcss + - supports-color + - typescript + - uglify-js + - utf-8-validate + - webpack-cli + + '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': dependencies: '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/logger': 3.9.2 @@ -10952,7 +10998,7 @@ snapshots: dependencies: '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) @@ -11000,7 +11046,7 @@ snapshots: '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 @@ -11040,7 +11086,7 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -11064,12 +11110,36 @@ snapshots: - uglify-js - webpack-cli + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/history': 4.7.11 + '@types/react': 18.3.23 + '@types/react-router-config': 5.0.11 + clsx: 2.1.1 + parse-numeric-range: 1.3.0 + prism-react-renderer: 2.4.1(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + utility-types: 3.11.0 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - supports-color + - uglify-js + - webpack-cli + '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.39.0)(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.6.3)': dependencies: '@docsearch/react': 4.2.0(@algolia/client-search@5.39.0)(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/logger': 3.9.2 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(debug@4.4.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@18.3.23)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -18686,6 +18756,8 @@ snapshots: dependencies: escape-goat: 4.0.0 + pure-rand@7.0.1: {} + qs@6.13.0: dependencies: side-channel: 1.1.0