@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.
@@ -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 { cleanJson } = require('../../utils');
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<Object>} Execution results including variables and test results
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
- // Create script context with all necessary variables
41
- const scriptContext = {
42
- // Bruno context
43
- console: context.console,
44
- req: context.req,
45
- res: context.res,
46
- bru: context.bru,
47
- expect: context.expect,
48
- assert: context.assert,
49
- __brunoTestResults: context.__brunoTestResults,
50
- test: context.test,
51
- // Configuration for nested module loading
52
- scriptingConfig: scriptingConfig,
53
- // Global objects
54
- Buffer: global.Buffer,
55
- process: global.process,
56
- setTimeout: global.setTimeout,
57
- setInterval: global.setInterval,
58
- clearTimeout: global.clearTimeout,
59
- clearInterval: global.clearInterval,
60
- setImmediate: global.setImmediate,
61
- clearImmediate: global.clearImmediate,
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
- // Create a custom require function and add it to the context
51
+ // Add require() function for CJS module loading
75
52
  scriptContext.require = createCustomRequire({
76
- scriptingConfig,
77
53
  collectionPath,
78
- scriptContext,
54
+ isolatedContext,
79
55
  currentModuleDir: collectionPath,
80
- localModuleCache
56
+ localModuleCache,
57
+ additionalContextRootsAbsolute
81
58
  });
82
59
 
83
- // Execute the script in an isolated VM context
84
- await vm.runInNewContext(`
85
- (async function(){
86
- ${script}
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
- * Creates a custom require function with enhanced security and local module support
102
- * @param {Object} options - Configuration options
103
- * @param {Object} options.scriptingConfig - Scripting configuration with additional context roots
104
- * @param {string} options.collectionPath - Base collection path for security checks
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 createCustomRequire({
111
- scriptingConfig,
112
- collectionPath,
113
- scriptContext,
114
- currentModuleDir = collectionPath,
115
- localModuleCache = new Map()
116
- }) {
117
- const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
118
- const additionalContextRootsAbsolute = lodash
119
- .chain(additionalContextRoots)
120
- .map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
121
- .value();
122
- additionalContextRootsAbsolute.push(collectionPath);
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
- // Resolve the file path relative to the current module's directory
168
- const filePath = path.resolve(currentModuleDir, resolvedFilename);
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
- // Cross-platform security check: ensure the resolved file is within collectionPath
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 = {