@usebruno/js 0.37.0 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebruno/js",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "license": "MIT",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -16,7 +16,7 @@
16
16
  "prepack": "npm run test"
17
17
  },
18
18
  "dependencies": {
19
- "@usebruno/common": "0.11.0",
19
+ "@usebruno/common": "0.12.1",
20
20
  "@usebruno/crypto-js": "^3.1.9",
21
21
  "@usebruno/query": "0.1.0",
22
22
  "ajv": "^8.12.0",
package/src/index.js CHANGED
@@ -2,10 +2,12 @@ const ScriptRuntime = require('./runtime/script-runtime');
2
2
  const TestRuntime = require('./runtime/test-runtime');
3
3
  const VarsRuntime = require('./runtime/vars-runtime');
4
4
  const AssertRuntime = require('./runtime/assert-runtime');
5
+ const { runScriptInNodeVm } = require('./sandbox/node-vm');
5
6
 
6
7
  module.exports = {
7
8
  ScriptRuntime,
8
9
  TestRuntime,
9
10
  VarsRuntime,
10
- AssertRuntime
11
+ AssertRuntime,
12
+ runScriptInNodeVm
11
13
  };
@@ -14,6 +14,7 @@ const BrunoRequest = require('../bruno-request');
14
14
  const BrunoResponse = require('../bruno-response');
15
15
  const { cleanJson } = require('../utils');
16
16
  const { createBruTestResultMethods } = require('../utils/results');
17
+ const { runScriptInNodeVm } = require('../sandbox/node-vm');
17
18
 
18
19
  // Inbuilt Library Support
19
20
  const ajv = require('ajv');
@@ -119,6 +120,28 @@ class ScriptRuntime {
119
120
  context.bru.runRequest = runRequestByItemPathname;
120
121
  }
121
122
 
123
+ if (this.runtime === 'nodevm') {
124
+ await runScriptInNodeVm({
125
+ script,
126
+ context,
127
+ collectionPath,
128
+ scriptingConfig
129
+ });
130
+
131
+ return {
132
+ request,
133
+ envVariables: cleanJson(envVariables),
134
+ runtimeVariables: cleanJson(runtimeVariables),
135
+ visualizations,
136
+ persistentEnvVariables: bru.persistentEnvVariables,
137
+ globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
138
+ results: cleanJson(__brunoTestResults.getResults()),
139
+ nextRequestName: bru.nextRequest,
140
+ skipRequest: bru.skipRequest,
141
+ stopExecution: bru.stopExecution
142
+ };
143
+ }
144
+
122
145
  if (this.runtime === 'quickjs') {
123
146
  await executeQuickJsVmAsync({
124
147
  script: script,
@@ -277,6 +300,28 @@ class ScriptRuntime {
277
300
  context.bru.runRequest = runRequestByItemPathname;
278
301
  }
279
302
 
303
+ if (this.runtime === 'nodevm') {
304
+ await runScriptInNodeVm({
305
+ script,
306
+ context,
307
+ collectionPath,
308
+ scriptingConfig
309
+ });
310
+
311
+ return {
312
+ response,
313
+ envVariables: cleanJson(envVariables),
314
+ persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
315
+ runtimeVariables: cleanJson(runtimeVariables),
316
+ visualizations,
317
+ globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),
318
+ results: cleanJson(__brunoTestResults.getResults()),
319
+ nextRequestName: bru.nextRequest,
320
+ skipRequest: bru.skipRequest,
321
+ stopExecution: bru.stopExecution
322
+ };
323
+ }
324
+
280
325
  if (this.runtime === 'quickjs') {
281
326
  await executeQuickJsVmAsync({
282
327
  script: script,
@@ -1,4 +1,5 @@
1
1
  const { NodeVM } = require('@usebruno/vm2');
2
+ const { runScriptInNodeVm } = require('../sandbox/node-vm');
2
3
  const chai = require('chai');
3
4
  const path = require('path');
4
5
  const http = require('http');
@@ -136,6 +137,13 @@ class TestRuntime {
136
137
  script: testsFile,
137
138
  context: context
138
139
  });
140
+ } else if (this.runtime === 'nodevm') {
141
+ await runScriptInNodeVm({
142
+ script: testsFile,
143
+ context,
144
+ collectionPath,
145
+ scriptingConfig
146
+ });
139
147
  } else {
140
148
  // default runtime is vm2
141
149
  const vm = new NodeVM({
@@ -0,0 +1,223 @@
1
+ const vm = require('node:vm');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { get } = require('lodash');
5
+ const lodash = require('lodash');
6
+ const { cleanJson } = require('../../utils');
7
+
8
+ class ScriptError extends Error {
9
+ constructor(error, script) {
10
+ super(error.message);
11
+ this.name = 'ScriptError';
12
+ this.originalError = error;
13
+ this.script = script;
14
+ this.stack = error.stack;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Executes a script in a Node.js VM context with enhanced security and module loading
20
+ * @param {Object} options - Configuration options
21
+ * @param {string} options.script - The script code to execute
22
+ * @param {Object} options.context - The execution context with Bruno objects
23
+ * @param {string} options.collectionPath - Path to the collection directory
24
+ * @param {Object} options.scriptingConfig - Scripting configuration options
25
+ * @returns {Promise<Object>} Execution results including variables and test results
26
+ * @throws {ScriptError} When script execution fails
27
+ */
28
+ async function runScriptInNodeVm({
29
+ script,
30
+ context,
31
+ collectionPath,
32
+ scriptingConfig
33
+ }) {
34
+ if (script.trim().length === 0) {
35
+ return;
36
+ }
37
+
38
+ try {
39
+ // Create script context with all necessary variables
40
+ const scriptContext = {
41
+ // Bruno context
42
+ console: context.console,
43
+ req: context.req,
44
+ res: context.res,
45
+ bru: context.bru,
46
+ expect: context.expect,
47
+ assert: context.assert,
48
+ __brunoTestResults: context.__brunoTestResults,
49
+ test: context.test,
50
+ // Configuration for nested module loading
51
+ scriptingConfig: scriptingConfig,
52
+ // Global objects
53
+ Buffer: global.Buffer,
54
+ process: global.process,
55
+ setTimeout: global.setTimeout,
56
+ setInterval: global.setInterval,
57
+ clearTimeout: global.clearTimeout,
58
+ clearInterval: global.clearInterval,
59
+ setImmediate: global.setImmediate,
60
+ clearImmediate: global.clearImmediate
61
+ };
62
+
63
+ // Create shared cache for local modules
64
+ const localModuleCache = new Map();
65
+
66
+ // Create a custom require function and add it to the context
67
+ scriptContext.require = createCustomRequire({
68
+ scriptingConfig,
69
+ collectionPath,
70
+ scriptContext,
71
+ currentModuleDir: collectionPath,
72
+ localModuleCache
73
+ });
74
+
75
+ // Execute the script in an isolated VM context
76
+ await vm.runInNewContext(`
77
+ (async function(){
78
+ ${script}
79
+ })();
80
+ `, scriptContext, {
81
+ filename: path.join(collectionPath, 'script.js'),
82
+ displayErrors: true
83
+ });
84
+ } catch (error) {
85
+ throw new ScriptError(error, script);
86
+ }
87
+
88
+ return;
89
+ }
90
+
91
+
92
+ /**
93
+ * Creates a custom require function with enhanced security and local module support
94
+ * @param {Object} options - Configuration options
95
+ * @param {Object} options.scriptingConfig - Scripting configuration with additional context roots
96
+ * @param {string} options.collectionPath - Base collection path for security checks
97
+ * @param {Object} options.scriptContext - Script execution context
98
+ * @param {string} options.currentModuleDir - Current module directory for relative imports
99
+ * @param {Map} options.localModuleCache - Cache for loaded local modules
100
+ * @returns {Function} Custom require function
101
+ */
102
+ function createCustomRequire({
103
+ scriptingConfig,
104
+ collectionPath,
105
+ scriptContext,
106
+ currentModuleDir = collectionPath,
107
+ localModuleCache = new Map()
108
+ }) {
109
+ const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
110
+ const additionalContextRootsAbsolute = lodash
111
+ .chain(additionalContextRoots)
112
+ .map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
113
+ .value();
114
+ additionalContextRootsAbsolute.push(collectionPath);
115
+
116
+ return (moduleName) => {
117
+ // Check if it's a local module (starts with ./ or ../)
118
+ if (moduleName.startsWith('./') || moduleName.startsWith('../')) {
119
+ return loadLocalModule({ moduleName, collectionPath, scriptContext, localModuleCache, currentModuleDir });
120
+ }
121
+
122
+ // First try to require as a native/npm module
123
+ try {
124
+ return require(moduleName);
125
+ } catch {
126
+ // If that fails, try to resolve from additionalContextRoots
127
+ try {
128
+ const modulePath = require.resolve(moduleName, { paths: additionalContextRootsAbsolute });
129
+ return require(modulePath);
130
+ } catch (error) {
131
+ throw new Error(`Could not resolve module "${moduleName}": ${error.message}\n\nThis most likely means you did not install the module under "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map(root => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
132
+ }
133
+ }
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Loads a local module from the filesystem with security checks and caching
139
+ * @param {Object} options - Configuration options
140
+ * @param {string} options.moduleName - Name/path of the module to load
141
+ * @param {string} options.collectionPath - Base collection path for security validation
142
+ * @param {Object} options.scriptContext - Script execution context to inherit
143
+ * @param {Map} options.localModuleCache - Cache for loaded modules
144
+ * @param {string} options.currentModuleDir - Directory of the current module for relative resolution
145
+ * @returns {*} The exported content of the loaded module
146
+ * @throws {Error} When module is outside collection path or cannot be loaded
147
+ */
148
+ function loadLocalModule({
149
+ moduleName,
150
+ collectionPath,
151
+ scriptContext,
152
+ localModuleCache,
153
+ currentModuleDir
154
+ }) {
155
+ // Check if the filename has an extension
156
+ const hasExtension = path.extname(moduleName) !== '';
157
+ const resolvedFilename = hasExtension ? moduleName : `${moduleName}.js`;
158
+
159
+ // Resolve the file path relative to the current module's directory
160
+ const filePath = path.resolve(currentModuleDir, resolvedFilename);
161
+ const normalizedFilePath = path.normalize(filePath);
162
+ const normalizedCollectionPath = path.normalize(collectionPath);
163
+
164
+ // Cross-platform security check: ensure the resolved file is within collectionPath
165
+ const relativePath = path.relative(normalizedCollectionPath, normalizedFilePath);
166
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
167
+ throw new Error(`Access to files outside of the collectionPath is not allowed: ${moduleName}`);
168
+ }
169
+
170
+ // Check cache first (use normalized path as key)
171
+ if (localModuleCache.has(normalizedFilePath)) {
172
+ return localModuleCache.get(normalizedFilePath);
173
+ }
174
+
175
+ if (!fs.existsSync(normalizedFilePath)) {
176
+ throw new Error(`Cannot find module ${moduleName}`);
177
+ }
178
+
179
+ // Read and execute the local module
180
+ const moduleCode = fs.readFileSync(normalizedFilePath, 'utf8');
181
+
182
+ // Create module object
183
+ const moduleObj = { exports: {} };
184
+
185
+ // Get the directory of this module for nested imports
186
+ const moduleDir = path.dirname(normalizedFilePath);
187
+
188
+ // Create a new context that inherits from the script context
189
+ const moduleContext = {
190
+ ...scriptContext,
191
+ module: moduleObj,
192
+ exports: moduleObj.exports,
193
+ __filename: normalizedFilePath,
194
+ __dirname: moduleDir,
195
+ // Create a custom require function for this module that resolves relative to its directory
196
+ require: createCustomRequire({
197
+ scriptingConfig: scriptContext.scriptingConfig || {},
198
+ collectionPath,
199
+ scriptContext,
200
+ currentModuleDir: moduleDir,
201
+ localModuleCache
202
+ })
203
+ };
204
+
205
+ try {
206
+ // Execute the module code in the shared context
207
+ vm.runInNewContext(moduleCode, moduleContext, {
208
+ filename: normalizedFilePath,
209
+ displayErrors: true
210
+ });
211
+
212
+ // Cache the result using normalized path
213
+ localModuleCache.set(normalizedFilePath, moduleObj.exports);
214
+
215
+ return moduleObj.exports;
216
+ } catch (error) {
217
+ throw new Error(`Error loading local module ${moduleName}: ${error.message}`);
218
+ }
219
+ }
220
+
221
+ module.exports = {
222
+ runScriptInNodeVm
223
+ };