@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 +2 -2
- package/src/index.js +3 -1
- package/src/runtime/script-runtime.js +45 -0
- package/src/runtime/test-runtime.js +8 -0
- package/src/sandbox/node-vm/index.js +223 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebruno/js",
|
|
3
|
-
"version": "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.
|
|
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
|
+
};
|