@usebruno/js 0.44.0 → 0.45.1
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 +3 -7
- package/src/bru.js +30 -5
- package/src/bruno-request.js +58 -0
- package/src/bruno-response.js +14 -0
- package/src/runtime/assert-runtime.js +10 -2
- package/src/runtime/script-runtime.js +4 -3
- package/src/runtime/test-runtime.js +2 -1
- package/src/runtime/vars-runtime.js +2 -1
- package/src/sandbox/node-vm/cjs-loader.js +369 -0
- package/src/sandbox/node-vm/constants.js +99 -0
- package/src/sandbox/node-vm/index.js +46 -222
- package/src/sandbox/node-vm/index.spec.js +950 -4
- package/src/sandbox/node-vm/utils.js +42 -0
- package/src/sandbox/quickjs/shims/bru.js +6 -0
- package/src/sandbox/quickjs/shims/bruno-request.js +27 -0
- package/src/sandbox/quickjs/shims/test.js +21 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for the Node.js VM sandbox.
|
|
3
|
+
*
|
|
4
|
+
* ECMAScript built-ins (Object, Array, Function, etc.)
|
|
5
|
+
* are NOT passed from the host. The VM provides its own versions, ensuring
|
|
6
|
+
* consistent prototype chains for libraries that use introspection.
|
|
7
|
+
*
|
|
8
|
+
* Handled separately in index.js:
|
|
9
|
+
* - global/globalThis: Points to isolated context (not host)
|
|
10
|
+
* - require: createCustomRequire() (custom module loader)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Safe globals to pass from host to VM context.
|
|
15
|
+
*
|
|
16
|
+
* ECMAScript built-ins (Object, Array, Function, String, Number,
|
|
17
|
+
* Boolean, Symbol, Date, RegExp, Map, Set, Promise, JSON, Math,
|
|
18
|
+
* parseInt, etc.) are intentionally NOT included here.
|
|
19
|
+
*
|
|
20
|
+
* The VM context provides its own versions of these, which ensures consistent
|
|
21
|
+
* prototype chains. Passing host versions causes prototype mismatches.
|
|
22
|
+
*
|
|
23
|
+
* Only Node.js-specific and Web APIs that the VM doesn't provide are listed.
|
|
24
|
+
*/
|
|
25
|
+
const safeGlobals = [
|
|
26
|
+
'process',
|
|
27
|
+
|
|
28
|
+
// Node.js timers (not part of ECMAScript)
|
|
29
|
+
'setTimeout',
|
|
30
|
+
'setInterval',
|
|
31
|
+
'clearTimeout',
|
|
32
|
+
'clearInterval',
|
|
33
|
+
'setImmediate',
|
|
34
|
+
'clearImmediate',
|
|
35
|
+
'queueMicrotask',
|
|
36
|
+
|
|
37
|
+
// Node.js globals
|
|
38
|
+
'Buffer',
|
|
39
|
+
|
|
40
|
+
// Error types - needed for instanceof checks with errors from host APIs/modules
|
|
41
|
+
'Error',
|
|
42
|
+
'TypeError',
|
|
43
|
+
'ReferenceError',
|
|
44
|
+
'SyntaxError',
|
|
45
|
+
'RangeError',
|
|
46
|
+
'URIError',
|
|
47
|
+
'EvalError',
|
|
48
|
+
'AggregateError',
|
|
49
|
+
|
|
50
|
+
// URL APIs (WHATWG - not ECMAScript)
|
|
51
|
+
'URL',
|
|
52
|
+
'URLSearchParams',
|
|
53
|
+
|
|
54
|
+
// Encoding APIs
|
|
55
|
+
'TextEncoder',
|
|
56
|
+
'TextDecoder',
|
|
57
|
+
'atob',
|
|
58
|
+
'btoa',
|
|
59
|
+
|
|
60
|
+
// Fetch API (Node 18+)
|
|
61
|
+
'fetch',
|
|
62
|
+
'Request',
|
|
63
|
+
'Response',
|
|
64
|
+
'Headers',
|
|
65
|
+
'FormData',
|
|
66
|
+
'AbortController',
|
|
67
|
+
'AbortSignal',
|
|
68
|
+
'Blob',
|
|
69
|
+
|
|
70
|
+
// Streams API
|
|
71
|
+
'ReadableStream',
|
|
72
|
+
'WritableStream',
|
|
73
|
+
'TransformStream',
|
|
74
|
+
|
|
75
|
+
// Internationalization (needs host's locale data)
|
|
76
|
+
'Intl',
|
|
77
|
+
|
|
78
|
+
// Web Crypto API
|
|
79
|
+
'crypto',
|
|
80
|
+
|
|
81
|
+
// WebAssembly
|
|
82
|
+
'WebAssembly',
|
|
83
|
+
|
|
84
|
+
// Performance API
|
|
85
|
+
'performance',
|
|
86
|
+
|
|
87
|
+
// Events API
|
|
88
|
+
'Event',
|
|
89
|
+
'EventTarget',
|
|
90
|
+
'CustomEvent',
|
|
91
|
+
|
|
92
|
+
// Message passing
|
|
93
|
+
'MessageChannel',
|
|
94
|
+
'MessagePort'
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
safeGlobals
|
|
99
|
+
};
|
|
@@ -1,44 +1,30 @@
|
|
|
1
1
|
const vm = require('node:vm');
|
|
2
|
-
const fs = require('node:fs');
|
|
3
2
|
const path = require('node:path');
|
|
4
3
|
const { get } = require('lodash');
|
|
5
4
|
const lodash = require('lodash');
|
|
5
|
+
const { ScriptError } = require('./utils');
|
|
6
|
+
const { createCustomRequire } = require('./cjs-loader');
|
|
7
|
+
const { safeGlobals } = require('./constants');
|
|
6
8
|
const { mixinTypedArrays } = require('../mixins/typed-arrays');
|
|
7
9
|
|
|
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
10
|
/**
|
|
19
11
|
* Executes a script in a Node.js VM context with enhanced security and module loading
|
|
12
|
+
*
|
|
20
13
|
* @param {Object} options - Configuration options
|
|
21
14
|
* @param {string} options.script - The script code to execute
|
|
22
15
|
* @param {Object} options.context - The execution context with Bruno objects
|
|
23
16
|
* @param {string} options.collectionPath - Path to the collection directory
|
|
24
17
|
* @param {Object} options.scriptingConfig - Scripting configuration options
|
|
25
|
-
* @returns {Promise<
|
|
18
|
+
* @returns {Promise<void>}
|
|
26
19
|
* @throws {ScriptError} When script execution fails
|
|
27
20
|
*/
|
|
28
|
-
async function runScriptInNodeVm({
|
|
29
|
-
script,
|
|
30
|
-
context,
|
|
31
|
-
collectionPath,
|
|
32
|
-
scriptingConfig
|
|
33
|
-
}) {
|
|
21
|
+
async function runScriptInNodeVm({ script, context, collectionPath, scriptingConfig }) {
|
|
34
22
|
if (script.trim().length === 0) {
|
|
35
23
|
return;
|
|
36
24
|
}
|
|
37
25
|
|
|
38
26
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Compute additional context roots
|
|
27
|
+
// Compute allowed context roots for security validation
|
|
42
28
|
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
43
29
|
const additionalContextRootsAbsolute = lodash
|
|
44
30
|
.chain(additionalContextRoots)
|
|
@@ -47,229 +33,67 @@ async function runScriptInNodeVm({
|
|
|
47
33
|
.value();
|
|
48
34
|
additionalContextRootsAbsolute.push(path.normalize(collectionPath));
|
|
49
35
|
|
|
50
|
-
//
|
|
51
|
-
const scriptContext =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
bru: context.bru,
|
|
57
|
-
expect: context.expect,
|
|
58
|
-
assert: context.assert,
|
|
59
|
-
__brunoTestResults: context.__brunoTestResults,
|
|
60
|
-
test: context.test,
|
|
61
|
-
// Configuration for nested module loading
|
|
62
|
-
scriptingConfig: scriptingConfig,
|
|
63
|
-
// Global objects
|
|
64
|
-
Buffer: global.Buffer,
|
|
65
|
-
process: global.process,
|
|
66
|
-
setTimeout: global.setTimeout,
|
|
67
|
-
setInterval: global.setInterval,
|
|
68
|
-
clearTimeout: global.clearTimeout,
|
|
69
|
-
clearInterval: global.clearInterval,
|
|
70
|
-
setImmediate: global.setImmediate,
|
|
71
|
-
clearImmediate: global.clearImmediate,
|
|
72
|
-
Error: global.Error,
|
|
73
|
-
TypeError: global.TypeError,
|
|
74
|
-
ReferenceError: global.ReferenceError,
|
|
75
|
-
SyntaxError: global.SyntaxError,
|
|
76
|
-
RangeError: global.RangeError
|
|
77
|
-
};
|
|
36
|
+
// Build the script context with Bruno objects and globals
|
|
37
|
+
const scriptContext = buildScriptContext(context, scriptingConfig);
|
|
38
|
+
|
|
39
|
+
// Create truly isolated context - scriptContext becomes the global object
|
|
40
|
+
// Scripts can ONLY access what's explicitly in scriptContext
|
|
41
|
+
const isolatedContext = vm.createContext(scriptContext);
|
|
78
42
|
|
|
79
|
-
|
|
43
|
+
// Add global/globalThis pointing to the isolated context (not host global)
|
|
44
|
+
// This allows libraries that reference 'global' to work while maintaining isolation
|
|
45
|
+
scriptContext.global = scriptContext;
|
|
46
|
+
scriptContext.globalThis = scriptContext;
|
|
80
47
|
|
|
81
|
-
// Create
|
|
48
|
+
// Create module cache for CJS modules
|
|
82
49
|
const localModuleCache = new Map();
|
|
83
50
|
|
|
84
|
-
//
|
|
51
|
+
// Add require() function for CJS module loading
|
|
85
52
|
scriptContext.require = createCustomRequire({
|
|
86
|
-
scriptingConfig,
|
|
87
53
|
collectionPath,
|
|
88
|
-
|
|
54
|
+
isolatedContext,
|
|
89
55
|
currentModuleDir: collectionPath,
|
|
90
56
|
localModuleCache,
|
|
91
|
-
allowScriptFilesystemAccess,
|
|
92
57
|
additionalContextRootsAbsolute
|
|
93
58
|
});
|
|
94
59
|
|
|
95
|
-
// Execute the script in
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
})();
|
|
100
|
-
`, scriptContext, {
|
|
101
|
-
filename: path.join(collectionPath, 'script.js'),
|
|
102
|
-
displayErrors: true
|
|
60
|
+
// Execute the script in the isolated context
|
|
61
|
+
const wrappedScript = `(async function(){ ${script} \n})();`;
|
|
62
|
+
const compiledScript = new vm.Script(wrappedScript, {
|
|
63
|
+
filename: path.join(collectionPath, 'script.js')
|
|
103
64
|
});
|
|
65
|
+
|
|
66
|
+
await compiledScript.runInContext(isolatedContext);
|
|
104
67
|
} catch (error) {
|
|
105
68
|
throw new ScriptError(error, script);
|
|
106
69
|
}
|
|
107
|
-
|
|
108
|
-
return;
|
|
109
70
|
}
|
|
110
71
|
|
|
111
72
|
/**
|
|
112
|
-
*
|
|
113
|
-
* @param {Object}
|
|
114
|
-
* @param {Object}
|
|
115
|
-
* @
|
|
116
|
-
* @param {Object} options.scriptContext - Script execution context
|
|
117
|
-
* @param {string} options.currentModuleDir - Current module directory for relative imports
|
|
118
|
-
* @param {Map} options.localModuleCache - Cache for loaded local modules
|
|
119
|
-
* @param {boolean} options.allowScriptFilesystemAccess - Whether to allow fs module access
|
|
120
|
-
* @param {Array<string>} options.additionalContextRootsAbsolute - Pre-computed absolute context roots
|
|
121
|
-
* @returns {Function} Custom require function
|
|
73
|
+
* Build the script context with Bruno objects and necessary globals
|
|
74
|
+
* @param {Object} context - Bruno context (bru, req, res, etc.)
|
|
75
|
+
* @param {Object} scriptingConfig - Scripting configuration
|
|
76
|
+
* @returns {Object} Script context object
|
|
122
77
|
*/
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (normalizedModuleName.startsWith('./') || normalizedModuleName.startsWith('../')) {
|
|
137
|
-
return loadLocalModule({ moduleName: normalizedModuleName, collectionPath, scriptContext, localModuleCache, currentModuleDir, additionalContextRootsAbsolute });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Helper function to check if a module is the fs module or a submodule
|
|
141
|
-
const isFsModule = (module) => {
|
|
142
|
-
if (!module) return false;
|
|
143
|
-
const fsModule = require('fs');
|
|
144
|
-
// Check if it's the fs module itself
|
|
145
|
-
if (module === fsModule) return true;
|
|
146
|
-
// Check if it's fs/promises submodule
|
|
147
|
-
if (module === fsModule.promises) return true;
|
|
148
|
-
// Check if it's fs/promises by comparing with require('fs/promises')
|
|
149
|
-
try {
|
|
150
|
-
if (module === require('fs/promises')) return true;
|
|
151
|
-
} catch {
|
|
152
|
-
// fs/promises might not be available in all Node versions
|
|
153
|
-
}
|
|
154
|
-
return false;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// First try to require as a native/npm module
|
|
158
|
-
try {
|
|
159
|
-
const requiredModulePath = require.resolve(moduleName, { paths: [...additionalContextRootsAbsolute, ...module.paths] });
|
|
160
|
-
const requiredModule = require(requiredModulePath);
|
|
161
|
-
|
|
162
|
-
// Block filesystem module access if filesystem access is not allowed
|
|
163
|
-
if (!allowScriptFilesystemAccess && isFsModule(requiredModule)) {
|
|
164
|
-
throw new Error('Filesystem access is not allowed. Enable "filesystemAccess.allow" in scripting config to use the fs module.');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return requiredModule;
|
|
168
|
-
} catch (requireError) {
|
|
169
|
-
// Re-throw if it's our filesystem access error
|
|
170
|
-
if (requireError.message && requireError.message.includes('Enable "filesystemAccess.allow"')) {
|
|
171
|
-
throw requireError;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// If that fails, try to resolve from additionalContextRoots
|
|
175
|
-
throw new Error(`Could not resolve module "${moduleName}": ${requireError.message}\n\nThis most likely means you did not install the module under the collection or the "additionalContextRoots" using a package manager like npm.\n\nThese are your current "additionalContextRoots":\n${additionalContextRootsAbsolute.map((root) => ` - ${root}`).join('\n') || ' - No "additionalContextRoots" defined'}`);
|
|
176
|
-
}
|
|
78
|
+
function buildScriptContext(context, scriptingConfig) {
|
|
79
|
+
const scriptContext = {
|
|
80
|
+
...context,
|
|
81
|
+
|
|
82
|
+
// Configuration for nested module loading
|
|
83
|
+
scriptingConfig: scriptingConfig,
|
|
84
|
+
|
|
85
|
+
// Safe globals from allowlist (Node.js/Web APIs only, not ECMAScript built-ins)
|
|
86
|
+
...Object.fromEntries(
|
|
87
|
+
safeGlobals
|
|
88
|
+
.filter((key) => global[key] !== undefined)
|
|
89
|
+
.map((key) => [key, global[key]])
|
|
90
|
+
)
|
|
177
91
|
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Loads a local module from the filesystem with security checks and caching
|
|
182
|
-
* @param {Object} options - Configuration options
|
|
183
|
-
* @param {string} options.moduleName - Name/path of the module to load
|
|
184
|
-
* @param {string} options.collectionPath - Base collection path for security validation
|
|
185
|
-
* @param {Object} options.scriptContext - Script execution context to inherit
|
|
186
|
-
* @param {Map} options.localModuleCache - Cache for loaded modules
|
|
187
|
-
* @param {string} options.currentModuleDir - Directory of the current module for relative resolution
|
|
188
|
-
* @param {Array<string>} options.additionalContextRootsAbsolute - Additional allowed context root paths
|
|
189
|
-
* @returns {*} The exported content of the loaded module
|
|
190
|
-
* @throws {Error} When module is outside collection path or cannot be loaded
|
|
191
|
-
*/
|
|
192
|
-
function loadLocalModule({
|
|
193
|
-
moduleName,
|
|
194
|
-
collectionPath,
|
|
195
|
-
scriptContext,
|
|
196
|
-
localModuleCache,
|
|
197
|
-
currentModuleDir,
|
|
198
|
-
additionalContextRootsAbsolute = []
|
|
199
|
-
}) {
|
|
200
|
-
// Check if the filename has an extension
|
|
201
|
-
const hasExtension = path.extname(moduleName) !== '';
|
|
202
|
-
const resolvedFilename = hasExtension ? moduleName : `${moduleName}.js`;
|
|
203
92
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
const normalizedFilePath = path.normalize(filePath);
|
|
93
|
+
// Add TypedArrays from host for compatibility with host APIs (TextEncoder, crypto, etc.)
|
|
94
|
+
mixinTypedArrays(scriptContext);
|
|
207
95
|
|
|
208
|
-
|
|
209
|
-
const normalizedAllowedRoot = path.normalize(allowedRoot);
|
|
210
|
-
const relativePath = path.relative(normalizedAllowedRoot, normalizedFilePath);
|
|
211
|
-
return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
if (!isWithinAllowedRoot) {
|
|
215
|
-
const allowedRootsDisplay = additionalContextRootsAbsolute.map((root) => ` - ${root}`).join('\n');
|
|
216
|
-
throw new Error(
|
|
217
|
-
`Access to files outside of the allowed context roots is not allowed: ${moduleName}\n\n`
|
|
218
|
-
+ `Allowed context roots:\n${allowedRootsDisplay}`
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Check cache first (use normalized path as key)
|
|
223
|
-
if (localModuleCache.has(normalizedFilePath)) {
|
|
224
|
-
return localModuleCache.get(normalizedFilePath);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if (!fs.existsSync(normalizedFilePath)) {
|
|
228
|
-
throw new Error(`Cannot find module ${moduleName}`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Read and execute the local module
|
|
232
|
-
const moduleCode = fs.readFileSync(normalizedFilePath, 'utf8');
|
|
233
|
-
|
|
234
|
-
// Create module object
|
|
235
|
-
const moduleObj = { exports: {} };
|
|
236
|
-
|
|
237
|
-
// Get the directory of this module for nested imports
|
|
238
|
-
const moduleDir = path.dirname(normalizedFilePath);
|
|
239
|
-
|
|
240
|
-
// Create a new context that inherits from the script context
|
|
241
|
-
const moduleContext = {
|
|
242
|
-
...scriptContext,
|
|
243
|
-
module: moduleObj,
|
|
244
|
-
exports: moduleObj.exports,
|
|
245
|
-
__filename: normalizedFilePath,
|
|
246
|
-
__dirname: moduleDir,
|
|
247
|
-
// Create a custom require function for this module that resolves relative to its directory
|
|
248
|
-
require: createCustomRequire({
|
|
249
|
-
scriptingConfig: scriptContext.scriptingConfig || {},
|
|
250
|
-
collectionPath,
|
|
251
|
-
scriptContext,
|
|
252
|
-
currentModuleDir: moduleDir,
|
|
253
|
-
localModuleCache,
|
|
254
|
-
allowScriptFilesystemAccess: get(scriptContext.scriptingConfig, 'filesystemAccess.allow', false),
|
|
255
|
-
additionalContextRootsAbsolute
|
|
256
|
-
})
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
// Execute the module code in the shared context
|
|
261
|
-
vm.runInNewContext(moduleCode, moduleContext, {
|
|
262
|
-
filename: normalizedFilePath,
|
|
263
|
-
displayErrors: true
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Cache the result using normalized path
|
|
267
|
-
localModuleCache.set(normalizedFilePath, moduleObj.exports);
|
|
268
|
-
|
|
269
|
-
return moduleObj.exports;
|
|
270
|
-
} catch (error) {
|
|
271
|
-
throw new Error(`Error loading local module ${moduleName}: ${error.message}`);
|
|
272
|
-
}
|
|
96
|
+
return scriptContext;
|
|
273
97
|
}
|
|
274
98
|
|
|
275
99
|
module.exports = {
|