@usebruno/js 0.43.0 → 0.45.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 +5 -12
- package/src/bru.js +37 -15
- package/src/bruno-request.js +66 -8
- package/src/bruno-response.js +15 -2
- package/src/runtime/assert-runtime.js +5 -2
- package/src/runtime/script-runtime.js +18 -207
- package/src/runtime/test-runtime.js +12 -106
- package/src/runtime/vars-runtime.js +3 -2
- 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 +58 -188
- package/src/sandbox/node-vm/index.spec.js +1198 -0
- package/src/sandbox/node-vm/utils.js +42 -0
- package/src/sandbox/quickjs/index.js +2 -2
- package/src/sandbox/quickjs/shims/bru.js +9 -3
- package/src/sandbox/quickjs/shims/bruno-request.js +27 -0
- package/src/sandbox/quickjs/shims/lib/crypto-utils.js +7 -9
- package/src/sandbox/quickjs/shims/lib/crypto-utils.spec.js +6 -6
- package/src/sandbox/quickjs/shims/lib/utils.js +1 -1
- package/src/utils/results.js +4 -4
- package/src/utils.js +6 -6
|
@@ -1,229 +1,99 @@
|
|
|
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');
|
|
6
|
-
const {
|
|
5
|
+
const { ScriptError } = require('./utils');
|
|
6
|
+
const { createCustomRequire } = require('./cjs-loader');
|
|
7
|
+
const { safeGlobals } = require('./constants');
|
|
7
8
|
const { mixinTypedArrays } = require('../mixins/typed-arrays');
|
|
8
9
|
|
|
9
|
-
class ScriptError extends Error {
|
|
10
|
-
constructor(error, script) {
|
|
11
|
-
super(error.message);
|
|
12
|
-
this.name = 'ScriptError';
|
|
13
|
-
this.originalError = error;
|
|
14
|
-
this.script = script;
|
|
15
|
-
this.stack = error.stack;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
10
|
/**
|
|
20
11
|
* Executes a script in a Node.js VM context with enhanced security and module loading
|
|
12
|
+
*
|
|
21
13
|
* @param {Object} options - Configuration options
|
|
22
14
|
* @param {string} options.script - The script code to execute
|
|
23
15
|
* @param {Object} options.context - The execution context with Bruno objects
|
|
24
16
|
* @param {string} options.collectionPath - Path to the collection directory
|
|
25
17
|
* @param {Object} options.scriptingConfig - Scripting configuration options
|
|
26
|
-
* @returns {Promise<
|
|
18
|
+
* @returns {Promise<void>}
|
|
27
19
|
* @throws {ScriptError} When script execution fails
|
|
28
20
|
*/
|
|
29
|
-
async function runScriptInNodeVm({
|
|
30
|
-
script,
|
|
31
|
-
context,
|
|
32
|
-
collectionPath,
|
|
33
|
-
scriptingConfig
|
|
34
|
-
}) {
|
|
21
|
+
async function runScriptInNodeVm({ script, context, collectionPath, scriptingConfig }) {
|
|
35
22
|
if (script.trim().length === 0) {
|
|
36
23
|
return;
|
|
37
24
|
}
|
|
38
25
|
|
|
39
26
|
try {
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Error: global.Error,
|
|
63
|
-
TypeError: global.TypeError,
|
|
64
|
-
ReferenceError: global.ReferenceError,
|
|
65
|
-
SyntaxError: global.SyntaxError,
|
|
66
|
-
RangeError: global.RangeError
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
mixinTypedArrays(scriptContext);
|
|
70
|
-
|
|
71
|
-
// Create shared cache for local modules
|
|
27
|
+
// Compute allowed context roots for security validation
|
|
28
|
+
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
29
|
+
const additionalContextRootsAbsolute = lodash
|
|
30
|
+
.chain(additionalContextRoots)
|
|
31
|
+
.map((acr) => (path.isAbsolute(acr) ? acr : path.join(collectionPath, acr)))
|
|
32
|
+
.map((acr) => path.normalize(acr))
|
|
33
|
+
.value();
|
|
34
|
+
additionalContextRootsAbsolute.push(path.normalize(collectionPath));
|
|
35
|
+
|
|
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);
|
|
42
|
+
|
|
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;
|
|
47
|
+
|
|
48
|
+
// Create module cache for CJS modules
|
|
72
49
|
const localModuleCache = new Map();
|
|
73
50
|
|
|
74
|
-
//
|
|
51
|
+
// Add require() function for CJS module loading
|
|
75
52
|
scriptContext.require = createCustomRequire({
|
|
76
|
-
scriptingConfig,
|
|
77
53
|
collectionPath,
|
|
78
|
-
|
|
54
|
+
isolatedContext,
|
|
79
55
|
currentModuleDir: collectionPath,
|
|
80
|
-
localModuleCache
|
|
56
|
+
localModuleCache,
|
|
57
|
+
additionalContextRootsAbsolute
|
|
81
58
|
});
|
|
82
59
|
|
|
83
|
-
// Execute the script in
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
})();
|
|
88
|
-
`, scriptContext, {
|
|
89
|
-
filename: path.join(collectionPath, 'script.js'),
|
|
90
|
-
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')
|
|
91
64
|
});
|
|
65
|
+
|
|
66
|
+
await compiledScript.runInContext(isolatedContext);
|
|
92
67
|
} catch (error) {
|
|
93
68
|
throw new ScriptError(error, script);
|
|
94
69
|
}
|
|
95
|
-
|
|
96
|
-
return;
|
|
97
70
|
}
|
|
98
71
|
|
|
99
|
-
|
|
100
72
|
/**
|
|
101
|
-
*
|
|
102
|
-
* @param {Object}
|
|
103
|
-
* @param {Object}
|
|
104
|
-
* @
|
|
105
|
-
* @param {Object} options.scriptContext - Script execution context
|
|
106
|
-
* @param {string} options.currentModuleDir - Current module directory for relative imports
|
|
107
|
-
* @param {Map} options.localModuleCache - Cache for loaded local modules
|
|
108
|
-
* @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
|
|
109
77
|
*/
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return (moduleName) => {
|
|
125
|
-
// Check if it's a local module (starts with ./ or ../)
|
|
126
|
-
if (moduleName.startsWith('./') || moduleName.startsWith('../')) {
|
|
127
|
-
return loadLocalModule({ moduleName, collectionPath, scriptContext, localModuleCache, currentModuleDir });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// First try to require as a native/npm module
|
|
131
|
-
try {
|
|
132
|
-
return require(moduleName);
|
|
133
|
-
} catch {
|
|
134
|
-
// If that fails, try to resolve from additionalContextRoots
|
|
135
|
-
try {
|
|
136
|
-
const modulePath = require.resolve(moduleName, { paths: additionalContextRootsAbsolute });
|
|
137
|
-
return require(modulePath);
|
|
138
|
-
} catch (error) {
|
|
139
|
-
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'}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
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
|
+
)
|
|
142
91
|
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Loads a local module from the filesystem with security checks and caching
|
|
147
|
-
* @param {Object} options - Configuration options
|
|
148
|
-
* @param {string} options.moduleName - Name/path of the module to load
|
|
149
|
-
* @param {string} options.collectionPath - Base collection path for security validation
|
|
150
|
-
* @param {Object} options.scriptContext - Script execution context to inherit
|
|
151
|
-
* @param {Map} options.localModuleCache - Cache for loaded modules
|
|
152
|
-
* @param {string} options.currentModuleDir - Directory of the current module for relative resolution
|
|
153
|
-
* @returns {*} The exported content of the loaded module
|
|
154
|
-
* @throws {Error} When module is outside collection path or cannot be loaded
|
|
155
|
-
*/
|
|
156
|
-
function loadLocalModule({
|
|
157
|
-
moduleName,
|
|
158
|
-
collectionPath,
|
|
159
|
-
scriptContext,
|
|
160
|
-
localModuleCache,
|
|
161
|
-
currentModuleDir
|
|
162
|
-
}) {
|
|
163
|
-
// Check if the filename has an extension
|
|
164
|
-
const hasExtension = path.extname(moduleName) !== '';
|
|
165
|
-
const resolvedFilename = hasExtension ? moduleName : `${moduleName}.js`;
|
|
166
92
|
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
const normalizedFilePath = path.normalize(filePath);
|
|
170
|
-
const normalizedCollectionPath = path.normalize(collectionPath);
|
|
93
|
+
// Add TypedArrays from host for compatibility with host APIs (TextEncoder, crypto, etc.)
|
|
94
|
+
mixinTypedArrays(scriptContext);
|
|
171
95
|
|
|
172
|
-
|
|
173
|
-
const relativePath = path.relative(normalizedCollectionPath, normalizedFilePath);
|
|
174
|
-
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
175
|
-
throw new Error(`Access to files outside of the collectionPath is not allowed: ${moduleName}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Check cache first (use normalized path as key)
|
|
179
|
-
if (localModuleCache.has(normalizedFilePath)) {
|
|
180
|
-
return localModuleCache.get(normalizedFilePath);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!fs.existsSync(normalizedFilePath)) {
|
|
184
|
-
throw new Error(`Cannot find module ${moduleName}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Read and execute the local module
|
|
188
|
-
const moduleCode = fs.readFileSync(normalizedFilePath, 'utf8');
|
|
189
|
-
|
|
190
|
-
// Create module object
|
|
191
|
-
const moduleObj = { exports: {} };
|
|
192
|
-
|
|
193
|
-
// Get the directory of this module for nested imports
|
|
194
|
-
const moduleDir = path.dirname(normalizedFilePath);
|
|
195
|
-
|
|
196
|
-
// Create a new context that inherits from the script context
|
|
197
|
-
const moduleContext = {
|
|
198
|
-
...scriptContext,
|
|
199
|
-
module: moduleObj,
|
|
200
|
-
exports: moduleObj.exports,
|
|
201
|
-
__filename: normalizedFilePath,
|
|
202
|
-
__dirname: moduleDir,
|
|
203
|
-
// Create a custom require function for this module that resolves relative to its directory
|
|
204
|
-
require: createCustomRequire({
|
|
205
|
-
scriptingConfig: scriptContext.scriptingConfig || {},
|
|
206
|
-
collectionPath,
|
|
207
|
-
scriptContext,
|
|
208
|
-
currentModuleDir: moduleDir,
|
|
209
|
-
localModuleCache
|
|
210
|
-
})
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
try {
|
|
214
|
-
// Execute the module code in the shared context
|
|
215
|
-
vm.runInNewContext(moduleCode, moduleContext, {
|
|
216
|
-
filename: normalizedFilePath,
|
|
217
|
-
displayErrors: true
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// Cache the result using normalized path
|
|
221
|
-
localModuleCache.set(normalizedFilePath, moduleObj.exports);
|
|
222
|
-
|
|
223
|
-
return moduleObj.exports;
|
|
224
|
-
} catch (error) {
|
|
225
|
-
throw new Error(`Error loading local module ${moduleName}: ${error.message}`);
|
|
226
|
-
}
|
|
96
|
+
return scriptContext;
|
|
227
97
|
}
|
|
228
98
|
|
|
229
99
|
module.exports = {
|