@usebruno/js 0.44.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebruno/js",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "license": "MIT",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -13,7 +13,7 @@
13
13
  "prepack": "npm run test"
14
14
  },
15
15
  "dependencies": {
16
- "@usebruno/common": "0.17.0",
16
+ "@usebruno/common": "0.18.0",
17
17
  "@usebruno/query": "0.2.0",
18
18
  "ajv": "^8.12.0",
19
19
  "ajv-formats": "^2.1.1",
@@ -29,8 +29,7 @@
29
29
  "lodash": "^4.17.21",
30
30
  "moment": "^2.29.4",
31
31
  "nanoid": "3.3.8",
32
- "node-fetch": "2.7.0",
33
- "node-vault": "^0.10.2",
32
+ "node-fetch": "^2.7.0",
34
33
  "path": "^0.12.7",
35
34
  "quickjs-emscripten": "^0.29.2",
36
35
  "tv4": "^1.3.0",
@@ -43,8 +42,5 @@
43
42
  "@rollup/plugin-node-resolve": "^15.0.1",
44
43
  "rollup": "3.29.5",
45
44
  "rollup-plugin-terser": "^7.0.2"
46
- },
47
- "overrides": {
48
- "@postman/tunnel-agent":"0.6.4"
49
45
  }
50
46
  }
package/src/bru.js CHANGED
@@ -2,13 +2,32 @@ const { cloneDeep } = require('lodash');
2
2
  const { uuid } = require('./utils');
3
3
  const xmlFormat = require('xml-formatter');
4
4
  const { interpolate: _interpolate } = require('@usebruno/common');
5
- const { sendRequest } = require('@usebruno/requests').scripting;
5
+ const { sendRequest, createSendRequest } = require('@usebruno/requests').scripting;
6
6
  const { jar: createCookieJar } = require('@usebruno/requests').cookies;
7
7
 
8
8
  const variableNameRegex = /^[\w-.]*$/;
9
9
 
10
10
  class Bru {
11
- constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables) {
11
+ /**
12
+ * @param {string} runtime - The runtime environment ('quickjs' or 'nodevm')
13
+ * @param {object} envVariables - Environment variables
14
+ * @param {object} runtimeVariables - Runtime variables
15
+ * @param {object} processEnvVars - Process environment variables
16
+ * @param {string} collectionPath - Path to the collection
17
+ * @param {function} historyLogger - History logger function
18
+ * @param {function} setVisualizations - Visualizations setter function
19
+ * @param {object} secretVariables - Secret variables
20
+ * @param {object} collectionVariables - Collection-level variables
21
+ * @param {object} folderVariables - Folder-level variables
22
+ * @param {object} requestVariables - Request-level variables
23
+ * @param {object} globalEnvironmentVariables - Global environment variables
24
+ * @param {object} oauth2CredentialVariables - OAuth2 credential variables
25
+ * @param {object} iterationDetails - Iteration details for runner
26
+ * @param {string} collectionName - Name of the collection
27
+ * @param {object} promptVariables - Prompt variables
28
+ * @param {object} certsAndProxyConfig - Configuration for bru.sendRequest (proxy, certs, TLS)
29
+ */
30
+ constructor(runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables, certsAndProxyConfig) {
12
31
  this.envVariables = envVariables || {};
13
32
  this.runtimeVariables = runtimeVariables || {};
14
33
  this.promptVariables = promptVariables || {};
@@ -23,7 +42,9 @@ class Bru {
23
42
  this.historyLogger = historyLogger;
24
43
  this.setVisualizations = setVisualizations;
25
44
  this.collectionName = collectionName;
26
- this.sendRequest = sendRequest;
45
+ // Use createSendRequest with config if provided, otherwise use default sendRequest
46
+ this.sendRequest = certsAndProxyConfig ? createSendRequest(certsAndProxyConfig) : sendRequest;
47
+ this.runtime = runtime;
27
48
  this.cookies = {
28
49
  jar: () => {
29
50
  const cookieJar = createCookieJar();
@@ -250,12 +271,12 @@ class Bru {
250
271
  this.historyLogger({
251
272
  uid: uuid(),
252
273
  type: 'setVar()',
253
- data: { key, value: this.interpolate(value) },
274
+ data: { key, value: value },
254
275
  createdAt: new Date().toISOString()
255
276
  });
256
277
  }
257
278
 
258
- this.runtimeVariables[key] = this.interpolate(value);
279
+ this.runtimeVariables[key] = value;
259
280
  }
260
281
 
261
282
  getVar(key) {
@@ -329,6 +350,10 @@ class Bru {
329
350
  getCollectionName() {
330
351
  return this.collectionName;
331
352
  }
353
+
354
+ isSafeMode() {
355
+ return this.runtime === 'quickjs';
356
+ }
332
357
  }
333
358
 
334
359
  class IterationDataManager {
@@ -22,6 +22,7 @@ class BrunoRequest {
22
22
  this.timeout = req.timeout;
23
23
  this.historyLogger = historyLogger;
24
24
  this.name = req.name;
25
+ this.pathParams = req.pathParams;
25
26
  this.tags = req.tags || [];
26
27
  /**
27
28
  * We automatically parse the JSON body if the content type is JSON
@@ -45,6 +46,53 @@ class BrunoRequest {
45
46
  this.req.url = url;
46
47
  }
47
48
 
49
+ getHost() {
50
+ try {
51
+ const url = new URL(this.req.url);
52
+ return url.host;
53
+ } catch (e) {
54
+ return '';
55
+ }
56
+ }
57
+
58
+ getPath() {
59
+ try {
60
+ const url = new URL(this.req.url);
61
+ let pathname = url.pathname;
62
+
63
+ // If path params exist, interpolate them into the pathname
64
+ if (this.req.pathParams && Array.isArray(this.req.pathParams)) {
65
+ pathname = pathname
66
+ .split('/')
67
+ .map((segment) => {
68
+ if (segment.startsWith(':')) {
69
+ const paramName = segment.slice(1);
70
+ const pathParam = this.req.pathParams.find((param) => param.name === paramName);
71
+ if (pathParam && pathParam.value) {
72
+ return pathParam.value;
73
+ }
74
+ }
75
+ return segment;
76
+ })
77
+ .join('/');
78
+ }
79
+
80
+ return pathname;
81
+ } catch (e) {
82
+ return '';
83
+ }
84
+ }
85
+
86
+ getQueryString() {
87
+ try {
88
+ const url = new URL(this.req.url);
89
+ // Return query string without the leading '?'
90
+ return url.search ? url.search.substring(1) : '';
91
+ } catch (e) {
92
+ return '';
93
+ }
94
+ }
95
+
48
96
  getMethod() {
49
97
  return this.req.method;
50
98
  }
@@ -212,6 +260,16 @@ class BrunoRequest {
212
260
  return this.req.name;
213
261
  }
214
262
 
263
+ getPathParams() {
264
+ const params = Array.isArray(this.req.pathParams) ? this.req.pathParams : [];
265
+
266
+ return params.map((param) => ({
267
+ name: param.name,
268
+ value: param.value,
269
+ type: param.type
270
+ }));
271
+ }
272
+
215
273
  /**
216
274
  * Get the tags associated with this request
217
275
  * @returns {Array<string>} Array of tag strings
@@ -55,6 +55,20 @@ class BrunoResponse {
55
55
  const clonedData = _.cloneDeep(data);
56
56
  this.res.data = clonedData;
57
57
  this.body = clonedData;
58
+
59
+ // Update dataBuffer to match the modified body
60
+ if (clonedData === null || clonedData === undefined) {
61
+ this.res.dataBuffer = Buffer.from('');
62
+ } else if (typeof clonedData === 'string') {
63
+ this.res.dataBuffer = Buffer.from(clonedData);
64
+ } else {
65
+ // For objects, stringify them
66
+ try {
67
+ this.res.dataBuffer = Buffer.from(JSON.stringify(clonedData));
68
+ } catch (e) {
69
+ this.res.dataBuffer = Buffer.from('');
70
+ }
71
+ }
58
72
  }
59
73
 
60
74
  // TODO: Refactor: dataBuffer size calculation should be handled in a shared utility so it can be passed and reused across the application
@@ -257,7 +257,9 @@ class AssertRuntime {
257
257
  return [];
258
258
  }
259
259
 
260
+ const certsAndProxyConfig = request?.certsAndProxyConfig;
260
261
  const bru = new Bru(
262
+ this.runtime,
261
263
  envVariables,
262
264
  runtimeVariables,
263
265
  processEnvVars,
@@ -272,7 +274,8 @@ class AssertRuntime {
272
274
  oauth2CredentialVariables,
273
275
  iterationDetails,
274
276
  undefined,
275
- promptVariables
277
+ promptVariables,
278
+ certsAndProxyConfig
276
279
  );
277
280
  const req = new BrunoRequest(request, historyLogger);
278
281
  const res = createResponseParser(response);
@@ -40,8 +40,8 @@ class ScriptRuntime {
40
40
  const promptVariables = request?.promptVariables || {};
41
41
  const iterationDetails = request?.runnerIterationDetails || {};
42
42
  const assertionResults = request?.assertionResults || [];
43
- // TODO please clean this up
44
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables);
43
+ const certsAndProxyConfig = request?.certsAndProxyConfig;
44
+ const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables, certsAndProxyConfig);
45
45
  const req = new BrunoRequest(request);
46
46
 
47
47
  // extend bru with result getter methods
@@ -145,7 +145,8 @@ class ScriptRuntime {
145
145
  const promptVariables = request?.promptVariables || {};
146
146
  const iterationDetails = request?.runnerIterationDetails || {};
147
147
  const assertionResults = request?.assertionResults || [];
148
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables);
148
+ const certsAndProxyConfig = request?.certsAndProxyConfig;
149
+ const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables, certsAndProxyConfig);
149
150
  const req = new BrunoRequest(request);
150
151
  const res = new BrunoResponse(response);
151
152
 
@@ -37,7 +37,8 @@ class TestRuntime {
37
37
  const promptVariables = request?.promptVariables || {};
38
38
  const iterationDetails = request?.runnerIterationDetails || {};
39
39
  const assertionResults = request?.assertionResults || [];
40
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables);
40
+ const certsAndProxyConfig = request?.certsAndProxyConfig;
41
+ const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables, certsAndProxyConfig);
41
42
  const req = new BrunoRequest(request, historyLogger);
42
43
  const res = new BrunoResponse(response);
43
44
 
@@ -37,7 +37,8 @@ class VarsRuntime {
37
37
  }
38
38
 
39
39
  const promptVariables = request?.promptVariables || {};
40
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVars, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, undefined, promptVariables);
40
+ const certsAndProxyConfig = request?.certsAndProxyConfig;
41
+ const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVars, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, undefined, promptVariables, certsAndProxyConfig);
41
42
  const req = new BrunoRequest(request, historyLogger);
42
43
  const res = createResponseParser(response);
43
44
 
@@ -0,0 +1,369 @@
1
+ const vm = require('node:vm');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const nodeModule = require('node:module');
5
+
6
+ const { isBuiltinModule, isPathWithinAllowedRoots } = require('./utils');
7
+
8
+ /**
9
+ * Resolve a local module path, handling files and directories
10
+ * Follows Node.js resolution algorithm:
11
+ * 1. Exact path (with extension)
12
+ * 2. Path + .js extension
13
+ * 3. Directory with package.json (main field)
14
+ * 4. Directory with index.js
15
+ * @param {string} fromDir - Directory to resolve from
16
+ * @param {string} moduleName - Module name/path
17
+ * @returns {string} Resolved absolute path
18
+ */
19
+ function resolveLocalModulePath(fromDir, moduleName) {
20
+ const basePath = path.resolve(fromDir, moduleName);
21
+
22
+ // 1. If has extension, use as-is
23
+ if (path.extname(moduleName)) {
24
+ return path.normalize(basePath);
25
+ }
26
+
27
+ // 2. Try with .js extension
28
+ const withJs = basePath + '.js';
29
+ if (fs.existsSync(withJs)) {
30
+ return path.normalize(withJs);
31
+ }
32
+
33
+ // 3. Check if it's a directory
34
+ if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) {
35
+ // 3a. Check for package.json with main field
36
+ const pkgPath = path.join(basePath, 'package.json');
37
+ if (fs.existsSync(pkgPath)) {
38
+ try {
39
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
40
+ if (pkg.main) {
41
+ const mainPath = path.resolve(basePath, pkg.main);
42
+ if (fs.existsSync(mainPath)) {
43
+ return path.normalize(mainPath);
44
+ }
45
+ }
46
+ } catch {
47
+ // Ignore JSON parse errors, fall through to index.js
48
+ }
49
+ }
50
+
51
+ // 3b. Check for index.js
52
+ const indexPath = path.join(basePath, 'index.js');
53
+ if (fs.existsSync(indexPath)) {
54
+ return path.normalize(indexPath);
55
+ }
56
+ }
57
+
58
+ // 4. Fall back to original path (will likely fail with file not found)
59
+ return path.normalize(basePath);
60
+ }
61
+
62
+ /**
63
+ * Creates a custom require function with enhanced security and local module support
64
+ * @param {Object} options - Configuration options
65
+ * @param {string} options.collectionPath - Path to the collection directory
66
+ * @param {Object} options.isolatedContext - The VM isolated context created with vm.createContext()
67
+ * @param {string} options.currentModuleDir - Current module directory for resolving relative paths
68
+ * @param {Map} options.localModuleCache - Cache for loaded modules
69
+ * @param {string[]} options.additionalContextRootsAbsolute - Additional allowed root paths
70
+ * @returns {Function} Custom require function
71
+ */
72
+ function createCustomRequire({
73
+ collectionPath,
74
+ isolatedContext,
75
+ currentModuleDir = collectionPath,
76
+ localModuleCache = new Map(),
77
+ additionalContextRootsAbsolute = []
78
+ }) {
79
+ return (moduleName) => {
80
+ const normalizedModuleName = moduleName.replace(/\\/g, '/');
81
+
82
+ // 1. Handle local modules (./path, ../path)
83
+ if (normalizedModuleName.startsWith('./') || normalizedModuleName.startsWith('../')) {
84
+ return loadLocalModule({
85
+ moduleName: normalizedModuleName,
86
+ collectionPath,
87
+ isolatedContext,
88
+ localModuleCache,
89
+ currentModuleDir,
90
+ additionalContextRootsAbsolute
91
+ });
92
+ }
93
+
94
+ // 2. Handle absolute paths - route through local module security checks
95
+ // This prevents bypassing additionalContextRoots by using absolute paths
96
+ if (path.isAbsolute(normalizedModuleName)) {
97
+ return loadLocalModule({
98
+ moduleName: normalizedModuleName,
99
+ collectionPath,
100
+ isolatedContext,
101
+ localModuleCache,
102
+ currentModuleDir,
103
+ additionalContextRootsAbsolute
104
+ });
105
+ }
106
+
107
+ // 3. Handle Node.js builtin modules
108
+ // Note: Builtins are loaded via native require, bypassing VM isolation.
109
+ // This is intentional - [`developer` mode] node-vm isolation need not be strict for builtins.
110
+ if (isBuiltinModule(moduleName)) {
111
+ return require(moduleName);
112
+ }
113
+
114
+ // 4. Handle npm modules - load INTO vm context
115
+ return loadNpmModule({
116
+ moduleName,
117
+ collectionPath,
118
+ isolatedContext,
119
+ localModuleCache
120
+ });
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Loads a local module from the filesystem with security checks and caching
126
+ * @param {Object} options - Configuration options
127
+ * @returns {*} The exported content of the loaded module
128
+ * @throws {Error} When module is outside collection path or cannot be loaded
129
+ */
130
+ function loadLocalModule({
131
+ moduleName,
132
+ collectionPath,
133
+ isolatedContext,
134
+ localModuleCache,
135
+ currentModuleDir,
136
+ additionalContextRootsAbsolute = []
137
+ }) {
138
+ // Validate the raw module name doesn't try to escape allowed roots
139
+ const preliminaryPath = path.resolve(currentModuleDir, moduleName);
140
+ if (!isPathWithinAllowedRoots(path.normalize(preliminaryPath), additionalContextRootsAbsolute)) {
141
+ const allowedRootsDisplay = additionalContextRootsAbsolute.map((root) => ` - ${root}`).join('\n');
142
+ throw new Error(
143
+ `Access to files outside of the allowed context roots is not allowed: ${moduleName}\n\n`
144
+ + `Allowed context roots:\n${allowedRootsDisplay}`
145
+ );
146
+ }
147
+
148
+ // Resolve the module path, handling files and directories
149
+ const normalizedFilePath = resolveLocalModulePath(currentModuleDir, moduleName);
150
+
151
+ // Final security check after resolution
152
+ if (!isPathWithinAllowedRoots(normalizedFilePath, additionalContextRootsAbsolute)) {
153
+ const allowedRootsDisplay = additionalContextRootsAbsolute.map((root) => ` - ${root}`).join('\n');
154
+ throw new Error(
155
+ `Access to files outside of the allowed context roots is not allowed: ${moduleName}\n\n`
156
+ + `Allowed context roots:\n${allowedRootsDisplay}`
157
+ );
158
+ }
159
+
160
+ // Check cache - we cache moduleObj, return its exports
161
+ if (localModuleCache.has(normalizedFilePath)) {
162
+ return localModuleCache.get(normalizedFilePath).exports;
163
+ }
164
+
165
+ if (!fs.existsSync(normalizedFilePath)) {
166
+ throw new Error(`Cannot find module ${moduleName}`);
167
+ }
168
+
169
+ const moduleCode = fs.readFileSync(normalizedFilePath, 'utf8');
170
+ const moduleObj = { exports: {} };
171
+ const moduleDir = path.dirname(normalizedFilePath);
172
+
173
+ // Pre-populate cache with moduleObj BEFORE execution to handle circular dependencies
174
+ // This allows re-entrant requires to get partial exports (Node.js behavior)
175
+ // We cache moduleObj (not moduleObj.exports) so that module.exports reassignment works
176
+ localModuleCache.set(normalizedFilePath, moduleObj);
177
+
178
+ // Create require function for nested imports
179
+ const moduleRequire = createCustomRequire({
180
+ collectionPath,
181
+ isolatedContext,
182
+ currentModuleDir: moduleDir,
183
+ localModuleCache,
184
+ additionalContextRootsAbsolute
185
+ });
186
+
187
+ try {
188
+ // Wrap module code in a function that receives CJS parameters
189
+ const wrappedCode = `(function(module, exports, require, __filename, __dirname) {\n${moduleCode}\n})`;
190
+ const compiledScript = new vm.Script(wrappedCode, { filename: normalizedFilePath });
191
+ const moduleFunction = compiledScript.runInContext(isolatedContext);
192
+ moduleFunction(moduleObj, moduleObj.exports, moduleRequire, normalizedFilePath, moduleDir);
193
+ return moduleObj.exports;
194
+ } catch (error) {
195
+ // Remove failed module from cache to allow retry
196
+ localModuleCache.delete(normalizedFilePath);
197
+ throw new Error(`Error loading local module ${moduleName}: ${error.message}`);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Executes a module in the VM context with caching and special file handling
203
+ * @param {Object} options - Configuration options
204
+ * @returns {*} The exported content of the loaded module
205
+ * @throws {Error} When module cannot be loaded
206
+ */
207
+ function executeModuleInVmContext({
208
+ resolvedPath,
209
+ moduleName,
210
+ isolatedContext,
211
+ collectionPath,
212
+ localModuleCache
213
+ }) {
214
+ // Check cache - we cache moduleObj, return its exports
215
+ if (localModuleCache.has(resolvedPath)) {
216
+ return localModuleCache.get(resolvedPath).exports;
217
+ }
218
+
219
+ // Native modules (.node files) - fall back to host require
220
+ // Note: This bypasses VM isolation for native addons.
221
+ // This is intentional - [`developer` mode] node-vm isolation need not be strict for native modules.
222
+ if (resolvedPath.endsWith('.node')) {
223
+ const result = require(resolvedPath);
224
+ // Wrap in moduleObj format for consistent cache retrieval
225
+ localModuleCache.set(resolvedPath, { exports: result });
226
+ return result;
227
+ }
228
+
229
+ // JSON files - parse directly
230
+ if (resolvedPath.endsWith('.json')) {
231
+ const jsonContent = fs.readFileSync(resolvedPath, 'utf8');
232
+ const result = JSON.parse(jsonContent);
233
+ // Wrap in moduleObj format for consistent cache retrieval
234
+ localModuleCache.set(resolvedPath, { exports: result });
235
+ return result;
236
+ }
237
+
238
+ // JavaScript files
239
+ const moduleSource = fs.readFileSync(resolvedPath, 'utf8');
240
+ const moduleDir = path.dirname(resolvedPath);
241
+ const moduleObj = { exports: {} };
242
+
243
+ // Pre-populate cache with moduleObj BEFORE execution to handle circular dependencies
244
+ // This allows re-entrant requires to get partial exports (Node.js behavior)
245
+ // We cache moduleObj (not moduleObj.exports) so that module.exports reassignment works
246
+ localModuleCache.set(resolvedPath, moduleObj);
247
+
248
+ const moduleRequire = createNpmModuleRequire({
249
+ collectionPath,
250
+ isolatedContext,
251
+ currentModuleDir: moduleDir,
252
+ localModuleCache
253
+ });
254
+
255
+ try {
256
+ // Wrap module code in a function that receives CJS parameters
257
+ const wrappedCode = `(function(module, exports, require, __filename, __dirname) {\n${moduleSource}\n})`;
258
+ const compiledScript = new vm.Script(wrappedCode, { filename: resolvedPath });
259
+ const moduleFunction = compiledScript.runInContext(isolatedContext);
260
+ moduleFunction(moduleObj, moduleObj.exports, moduleRequire, resolvedPath, moduleDir);
261
+ } catch (error) {
262
+ // Remove failed module from cache to allow retry
263
+ localModuleCache.delete(resolvedPath);
264
+ const stack = error.stack || '';
265
+ throw new Error(`Error loading module ${moduleName}: ${error.message}\nStack: ${stack}`);
266
+ }
267
+
268
+ return moduleObj.exports;
269
+ }
270
+
271
+ /**
272
+ * Loads an npm module into the vm context
273
+ * @param {Object} options - Configuration options
274
+ * @returns {*} The exported content of the loaded module
275
+ * @throws {Error} When module cannot be resolved or loaded
276
+ */
277
+ function loadNpmModule({
278
+ moduleName,
279
+ collectionPath,
280
+ isolatedContext,
281
+ localModuleCache
282
+ }) {
283
+ let resolvedPath;
284
+
285
+ // Module resolution order:
286
+ // 1. Collection's node_modules (user-installed packages for their collection)
287
+ // 2. Bruno's node_modules (fallback for built-in dependencies)
288
+ //
289
+ // This order ensures user packages take precedence, allowing users to:
290
+ // - Override Bruno's bundled package versions
291
+ // - Install collection-specific dependencies
292
+ if (collectionPath) {
293
+ try {
294
+ const collectionRequire = nodeModule.createRequire(path.join(collectionPath, 'package.json'));
295
+ resolvedPath = collectionRequire.resolve(moduleName);
296
+ } catch {
297
+ // Module not found in collection, continue to fallback
298
+ }
299
+ }
300
+
301
+ // Fall back to Bruno's node_modules
302
+ if (!resolvedPath) {
303
+ try {
304
+ resolvedPath = require.resolve(moduleName, { paths: module.paths });
305
+ } catch (mainError) {
306
+ throw new Error(
307
+ `Could not resolve module "${moduleName}": ${mainError.message}\n\n`
308
+ + `Install it with: npm install ${moduleName}`
309
+ );
310
+ }
311
+ }
312
+
313
+ return executeModuleInVmContext({
314
+ resolvedPath,
315
+ moduleName,
316
+ isolatedContext,
317
+ collectionPath,
318
+ localModuleCache
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Creates require function for npm module dependencies
324
+ * @param {Object} options - Configuration options
325
+ * @returns {Function} Custom require function for npm module dependencies
326
+ */
327
+ function createNpmModuleRequire({
328
+ collectionPath,
329
+ isolatedContext,
330
+ currentModuleDir,
331
+ localModuleCache
332
+ }) {
333
+ const moduleRequire = nodeModule.createRequire(path.join(currentModuleDir, 'index.js'));
334
+
335
+ return (moduleName) => {
336
+ // Handle relative imports within npm module
337
+ if (moduleName.startsWith('./') || moduleName.startsWith('../')) {
338
+ const resolvedPath = moduleRequire.resolve(moduleName);
339
+ return executeModuleInVmContext({
340
+ resolvedPath,
341
+ moduleName,
342
+ isolatedContext,
343
+ collectionPath,
344
+ localModuleCache
345
+ });
346
+ }
347
+
348
+ // Handle builtins
349
+ // Note: Builtins are loaded via native require, bypassing VM isolation.
350
+ // This is intentional - [`developer` mode] node-vm isolation need not be strict for builtins.
351
+ if (isBuiltinModule(moduleName)) {
352
+ return require(moduleName);
353
+ }
354
+
355
+ // Handle npm dependencies - resolve from current module's directory
356
+ const resolvedPath = moduleRequire.resolve(moduleName);
357
+ return executeModuleInVmContext({
358
+ resolvedPath,
359
+ moduleName,
360
+ isolatedContext,
361
+ collectionPath,
362
+ localModuleCache
363
+ });
364
+ };
365
+ }
366
+
367
+ module.exports = {
368
+ createCustomRequire
369
+ };