@usebruno/js 0.42.2 → 0.44.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,47 +1,17 @@
1
- const { NodeVM } = require('@usebruno/vm2');
2
- const { runScriptInNodeVm } = require('../sandbox/node-vm');
3
1
  const chai = require('chai');
4
- const path = require('path');
5
- const http = require('http');
6
- const https = require('https');
7
- const stream = require('stream');
8
- const util = require('util');
9
- const zlib = require('zlib');
10
- const url = require('url');
11
- const punycode = require('punycode');
12
- const fs = require('fs');
13
- const { get } = require('lodash');
2
+ const uuid = require('uuid');
14
3
  const Bru = require('../bru');
15
4
  const BrunoRequest = require('../bruno-request');
16
5
  const BrunoResponse = require('../bruno-response');
17
- const Test = require('../test');
18
- const TestResults = require('../test-results');
19
6
  const { cleanJson } = require('../utils');
20
7
  const { createBruTestResultMethods } = require('../utils/results');
21
-
22
- // Inbuilt Library Support
23
- const ajv = require('ajv');
24
- const addFormats = require('ajv-formats');
25
- const atob = require('atob');
26
- const btoa = require('btoa');
27
- const lodash = require('lodash');
28
- const moment = require('moment');
29
- const uuid = require('uuid');
30
- const nanoid = require('nanoid');
31
- const axios = require('axios');
32
- const fetch = require('node-fetch');
33
- const CryptoJS = require('crypto-js');
34
- const NodeVault = require('node-vault');
35
- const xml2js = require('xml2js');
36
- const cheerio = require('cheerio');
37
- const tv4 = require('tv4');
8
+ const { runScriptInNodeVm } = require('../sandbox/node-vm');
38
9
  const jsonwebtoken = require('jsonwebtoken');
39
10
  const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
40
- const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
41
11
 
42
12
  class TestRuntime {
43
13
  constructor(props) {
44
- this.runtime = props?.runtime || 'vm2';
14
+ this.runtime = props?.runtime || 'quickjs';
45
15
  }
46
16
 
47
17
  async runTests(
@@ -64,29 +34,12 @@ class TestRuntime {
64
34
  const collectionVariables = request?.collectionVariables || {};
65
35
  const folderVariables = request?.folderVariables || {};
66
36
  const requestVariables = request?.requestVariables || {};
37
+ const promptVariables = request?.promptVariables || {};
67
38
  const iterationDetails = request?.runnerIterationDetails || {};
68
39
  const assertionResults = request?.assertionResults || [];
69
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName);
40
+ const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables);
70
41
  const req = new BrunoRequest(request, historyLogger);
71
42
  const res = new BrunoResponse(response);
72
- const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
73
- const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
74
- const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
75
- const additionalContextRootsAbsolute = lodash
76
- .chain(additionalContextRoots)
77
- .map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
78
- .value();
79
-
80
- const whitelistedModules = {};
81
-
82
- for (let module of moduleWhitelist) {
83
- try {
84
- whitelistedModules[module] = require(module);
85
- } catch (e) {
86
- // Ignore
87
- console.warn(e);
88
- }
89
- }
90
43
 
91
44
  // extend bru with result getter methods
92
45
  const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
@@ -113,10 +66,6 @@ class TestRuntime {
113
66
  jwt: jsonwebtoken
114
67
  };
115
68
 
116
- if (this.runtime === 'vm2') {
117
- mixinTypedArrays(context);
118
- }
119
-
120
69
  if (onConsoleLog && typeof onConsoleLog === 'function') {
121
70
  const customLogger = (type) => {
122
71
  return (...args) => {
@@ -132,20 +81,14 @@ class TestRuntime {
132
81
  };
133
82
  }
134
83
 
135
- if(runRequestByItemPathname) {
84
+ if (runRequestByItemPathname) {
136
85
  context.bru.runRequest = runRequestByItemPathname;
137
86
  }
138
87
 
139
88
  let scriptError = null;
140
89
 
141
90
  try {
142
- if (this.runtime === 'quickjs') {
143
- await executeQuickJsVmAsync({
144
- script: testsFile,
145
- context: context,
146
- collectionPath
147
- });
148
- } else if (this.runtime === 'nodevm') {
91
+ if (this.runtime === 'nodevm') {
149
92
  await runScriptInNodeVm({
150
93
  script: testsFile,
151
94
  context,
@@ -153,49 +96,12 @@ class TestRuntime {
153
96
  scriptingConfig
154
97
  });
155
98
  } else {
156
- // default runtime is vm2
157
- const vm = new NodeVM({
158
- sandbox: context,
159
- require: {
160
- context: 'sandbox',
161
- external: true,
162
- builtin: ['*'],
163
- root: [collectionPath, ...additionalContextRootsAbsolute],
164
- mock: {
165
- // node libs
166
- path,
167
- stream,
168
- util,
169
- url,
170
- http,
171
- https,
172
- punycode,
173
- zlib,
174
- // 3rd party libs
175
- ajv,
176
- 'ajv-formats': addFormats,
177
- btoa,
178
- atob,
179
- lodash,
180
- moment,
181
- uuid,
182
- nanoid,
183
- axios,
184
- chai,
185
- 'node-fetch': fetch,
186
- 'crypto-js': CryptoJS,
187
- 'xml2js': xml2js,
188
- cheerio,
189
- tv4,
190
- 'jsonwebtoken': jsonwebtoken,
191
- ...whitelistedModules,
192
- fs: allowScriptFilesystemAccess ? fs : undefined,
193
- 'node-vault': NodeVault
194
- }
195
- }
99
+ // default runtime is `quickjs`
100
+ await executeQuickJsVmAsync({
101
+ script: testsFile,
102
+ context: context,
103
+ collectionPath
196
104
  });
197
- const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
198
- await asyncVM();
199
105
  }
200
106
  } catch (error) {
201
107
  scriptError = error;
@@ -20,7 +20,7 @@ const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
20
20
 
21
21
  class VarsRuntime {
22
22
  constructor(props) {
23
- this.runtime = props?.runtime || 'vm2';
23
+ this.runtime = props?.runtime || 'quickjs';
24
24
  this.mode = props?.mode || 'developer';
25
25
  }
26
26
 
@@ -36,7 +36,8 @@ class VarsRuntime {
36
36
  return;
37
37
  }
38
38
 
39
- const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, undefined, secretVars, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails);
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
41
  const req = new BrunoRequest(request, historyLogger);
41
42
  const res = createResponseParser(response);
42
43
 
@@ -3,7 +3,6 @@ const fs = require('node:fs');
3
3
  const path = require('node:path');
4
4
  const { get } = require('lodash');
5
5
  const lodash = require('lodash');
6
- const { cleanJson } = require('../../utils');
7
6
  const { mixinTypedArrays } = require('../mixins/typed-arrays');
8
7
 
9
8
  class ScriptError extends Error {
@@ -37,6 +36,17 @@ async function runScriptInNodeVm({
37
36
  }
38
37
 
39
38
  try {
39
+ const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
40
+
41
+ // Compute additional context roots
42
+ const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
43
+ const additionalContextRootsAbsolute = lodash
44
+ .chain(additionalContextRoots)
45
+ .map((acr) => (path.isAbsolute(acr) ? acr : path.join(collectionPath, acr)))
46
+ .map((acr) => path.normalize(acr))
47
+ .value();
48
+ additionalContextRootsAbsolute.push(path.normalize(collectionPath));
49
+
40
50
  // Create script context with all necessary variables
41
51
  const scriptContext = {
42
52
  // Bruno context
@@ -58,7 +68,12 @@ async function runScriptInNodeVm({
58
68
  clearTimeout: global.clearTimeout,
59
69
  clearInterval: global.clearInterval,
60
70
  setImmediate: global.setImmediate,
61
- clearImmediate: global.clearImmediate
71
+ clearImmediate: global.clearImmediate,
72
+ Error: global.Error,
73
+ TypeError: global.TypeError,
74
+ ReferenceError: global.ReferenceError,
75
+ SyntaxError: global.SyntaxError,
76
+ RangeError: global.RangeError
62
77
  };
63
78
 
64
79
  mixinTypedArrays(scriptContext);
@@ -72,7 +87,9 @@ async function runScriptInNodeVm({
72
87
  collectionPath,
73
88
  scriptContext,
74
89
  currentModuleDir: collectionPath,
75
- localModuleCache
90
+ localModuleCache,
91
+ allowScriptFilesystemAccess,
92
+ additionalContextRootsAbsolute
76
93
  });
77
94
 
78
95
  // Execute the script in an isolated VM context
@@ -91,7 +108,6 @@ async function runScriptInNodeVm({
91
108
  return;
92
109
  }
93
110
 
94
-
95
111
  /**
96
112
  * Creates a custom require function with enhanced security and local module support
97
113
  * @param {Object} options - Configuration options
@@ -100,6 +116,8 @@ async function runScriptInNodeVm({
100
116
  * @param {Object} options.scriptContext - Script execution context
101
117
  * @param {string} options.currentModuleDir - Current module directory for relative imports
102
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
103
121
  * @returns {Function} Custom require function
104
122
  */
105
123
  function createCustomRequire({
@@ -107,32 +125,54 @@ function createCustomRequire({
107
125
  collectionPath,
108
126
  scriptContext,
109
127
  currentModuleDir = collectionPath,
110
- localModuleCache = new Map()
128
+ localModuleCache = new Map(),
129
+ allowScriptFilesystemAccess = false,
130
+ additionalContextRootsAbsolute = []
111
131
  }) {
112
- const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
113
- const additionalContextRootsAbsolute = lodash
114
- .chain(additionalContextRoots)
115
- .map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
116
- .value();
117
- additionalContextRootsAbsolute.push(collectionPath);
118
-
119
132
  return (moduleName) => {
120
- // Check if it's a local module (starts with ./ or ../)
121
- if (moduleName.startsWith('./') || moduleName.startsWith('../')) {
122
- return loadLocalModule({ moduleName, collectionPath, scriptContext, localModuleCache, currentModuleDir });
133
+ // Check if it's a local module (starts with ./ or ../ or .\ or ..\)
134
+ // Normalize backslashes to forward slashes for cross-platform compatibility
135
+ const normalizedModuleName = moduleName.replace(/\\/g, '/');
136
+ if (normalizedModuleName.startsWith('./') || normalizedModuleName.startsWith('../')) {
137
+ return loadLocalModule({ moduleName: normalizedModuleName, collectionPath, scriptContext, localModuleCache, currentModuleDir, additionalContextRootsAbsolute });
123
138
  }
124
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
+
125
157
  // First try to require as a native/npm module
126
158
  try {
127
- return require(moduleName);
128
- } catch {
129
- // If that fails, try to resolve from additionalContextRoots
130
- try {
131
- const modulePath = require.resolve(moduleName, { paths: additionalContextRootsAbsolute });
132
- return require(modulePath);
133
- } catch (error) {
134
- 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'}`);
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.');
135
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'}`);
136
176
  }
137
177
  };
138
178
  }
@@ -145,6 +185,7 @@ function createCustomRequire({
145
185
  * @param {Object} options.scriptContext - Script execution context to inherit
146
186
  * @param {Map} options.localModuleCache - Cache for loaded modules
147
187
  * @param {string} options.currentModuleDir - Directory of the current module for relative resolution
188
+ * @param {Array<string>} options.additionalContextRootsAbsolute - Additional allowed context root paths
148
189
  * @returns {*} The exported content of the loaded module
149
190
  * @throws {Error} When module is outside collection path or cannot be loaded
150
191
  */
@@ -153,7 +194,8 @@ function loadLocalModule({
153
194
  collectionPath,
154
195
  scriptContext,
155
196
  localModuleCache,
156
- currentModuleDir
197
+ currentModuleDir,
198
+ additionalContextRootsAbsolute = []
157
199
  }) {
158
200
  // Check if the filename has an extension
159
201
  const hasExtension = path.extname(moduleName) !== '';
@@ -162,12 +204,19 @@ function loadLocalModule({
162
204
  // Resolve the file path relative to the current module's directory
163
205
  const filePath = path.resolve(currentModuleDir, resolvedFilename);
164
206
  const normalizedFilePath = path.normalize(filePath);
165
- const normalizedCollectionPath = path.normalize(collectionPath);
166
207
 
167
- // Cross-platform security check: ensure the resolved file is within collectionPath
168
- const relativePath = path.relative(normalizedCollectionPath, normalizedFilePath);
169
- if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
170
- throw new Error(`Access to files outside of the collectionPath is not allowed: ${moduleName}`);
208
+ const isWithinAllowedRoot = additionalContextRootsAbsolute.some((allowedRoot) => {
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
+ );
171
220
  }
172
221
 
173
222
  // Check cache first (use normalized path as key)
@@ -197,11 +246,13 @@ function loadLocalModule({
197
246
  __dirname: moduleDir,
198
247
  // Create a custom require function for this module that resolves relative to its directory
199
248
  require: createCustomRequire({
200
- scriptingConfig: scriptContext.scriptingConfig || {},
201
- collectionPath,
202
- scriptContext,
203
- currentModuleDir: moduleDir,
204
- localModuleCache
249
+ scriptingConfig: scriptContext.scriptingConfig || {},
250
+ collectionPath,
251
+ scriptContext,
252
+ currentModuleDir: moduleDir,
253
+ localModuleCache,
254
+ allowScriptFilesystemAccess: get(scriptContext.scriptingConfig, 'filesystemAccess.allow', false),
255
+ additionalContextRootsAbsolute
205
256
  })
206
257
  };
207
258
 
@@ -0,0 +1,252 @@
1
+ const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const { runScriptInNodeVm } = require('./index');
6
+
7
+ describe('node-vm sandbox', () => {
8
+ let testDir;
9
+ let collectionPath;
10
+
11
+ beforeEach(() => {
12
+ // Create a temporary test directory
13
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-test-'));
14
+ collectionPath = path.join(testDir, 'collection');
15
+ fs.mkdirSync(collectionPath);
16
+ });
17
+
18
+ afterEach(() => {
19
+ // Clean up test directory
20
+ fs.rmSync(testDir, { recursive: true, force: true });
21
+ });
22
+
23
+ describe('createCustomRequire - local modules', () => {
24
+ it('should load local module with ./ path', async () => {
25
+ // Create a local module
26
+ fs.writeFileSync(
27
+ path.join(collectionPath, 'helper.js'),
28
+ 'module.exports = { value: 42 };'
29
+ );
30
+
31
+ const script = `
32
+ const helper = require('./helper');
33
+ bru.setVar('result', helper.value);
34
+ `;
35
+
36
+ const context = {
37
+ bru: { setVar: jest.fn() },
38
+ console: console
39
+ };
40
+
41
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
42
+
43
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 42);
44
+ });
45
+
46
+ it('should load local module with ../ path', async () => {
47
+ // Create a subdirectory and modules
48
+ const subDir = path.join(collectionPath, 'subdir');
49
+ fs.mkdirSync(subDir);
50
+ fs.writeFileSync(
51
+ path.join(collectionPath, 'parent.js'),
52
+ 'module.exports = { name: "parent" };'
53
+ );
54
+ fs.writeFileSync(
55
+ path.join(subDir, 'child.js'),
56
+ 'const parent = require("../parent"); module.exports = parent;'
57
+ );
58
+
59
+ const script = `
60
+ const child = require('./subdir/child');
61
+ bru.setVar('result', child.name);
62
+ `;
63
+
64
+ const context = {
65
+ bru: { setVar: jest.fn() },
66
+ console: console
67
+ };
68
+
69
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
70
+
71
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 'parent');
72
+ });
73
+
74
+ it('should handle backslashes on Windows', async () => {
75
+ const subDir = path.join(collectionPath, 'utils');
76
+ fs.mkdirSync(subDir);
77
+ fs.writeFileSync(
78
+ path.join(subDir, 'module.js'),
79
+ 'module.exports = { platform: "cross-platform" };'
80
+ );
81
+
82
+ // Simulate Windows-style path with backslashes
83
+ const script = `
84
+ const mod = require('.\\\\utils\\\\module');
85
+ bru.setVar('result', mod.platform);
86
+ `;
87
+
88
+ const context = {
89
+ bru: { setVar: jest.fn() },
90
+ console: console
91
+ };
92
+
93
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
94
+
95
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 'cross-platform');
96
+ });
97
+
98
+ it('should block access outside collection path', async () => {
99
+ const script = `
100
+ const outside = require('../../outside');
101
+ `;
102
+
103
+ const context = { console: console };
104
+
105
+ await expect(
106
+ runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })
107
+ ).rejects.toThrow('Access to files outside of the allowed context roots is not allowed');
108
+ });
109
+ });
110
+
111
+ describe('createCustomRequire - additionalContextRoots', () => {
112
+ it('should allow module access from additionalContextRoots', async () => {
113
+ // Create an additional context root at same level as collection
114
+ const additionalRoot = path.join(testDir, 'shared');
115
+ fs.mkdirSync(additionalRoot);
116
+ fs.writeFileSync(
117
+ path.join(additionalRoot, 'shared.js'),
118
+ 'module.exports = { shared: true };'
119
+ );
120
+
121
+ // From collection, traverse up to testDir, then into shared directory
122
+ const script = `
123
+ const shared = require('../shared/shared');
124
+ bru.setVar('result', shared.shared);
125
+ `;
126
+
127
+ const context = {
128
+ bru: { setVar: jest.fn() },
129
+ console: console
130
+ };
131
+
132
+ const scriptingConfig = {
133
+ additionalContextRoots: [additionalRoot]
134
+ };
135
+
136
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
137
+
138
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
139
+ });
140
+
141
+ it('should handle relative additionalContextRoots path', async () => {
142
+ // Create a sibling directory to collection
143
+ const libsDir = path.join(testDir, 'libs');
144
+ fs.mkdirSync(libsDir);
145
+ fs.writeFileSync(
146
+ path.join(libsDir, 'lib.js'),
147
+ 'module.exports = { fromLib: "yes" };'
148
+ );
149
+
150
+ const script = `
151
+ const lib = require('../libs/lib');
152
+ bru.setVar('result', lib.fromLib);
153
+ `;
154
+
155
+ const context = {
156
+ bru: { setVar: jest.fn() },
157
+ console: console
158
+ };
159
+
160
+ const scriptingConfig = {
161
+ additionalContextRoots: ['../libs']
162
+ };
163
+
164
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
165
+
166
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 'yes');
167
+ });
168
+
169
+ it('should handle nested additional context roots modules', async () => {
170
+ // Create an additional context root
171
+ const additionalRoot = path.join(testDir, 'shared');
172
+ fs.mkdirSync(additionalRoot);
173
+ fs.writeFileSync(
174
+ path.join(additionalRoot, 'allowed.js'),
175
+ 'module.exports = { allowed: true };'
176
+ );
177
+
178
+ // Create a nested module that tries to require from additional root
179
+ fs.writeFileSync(
180
+ path.join(collectionPath, 'parent.js'),
181
+ `
182
+ const allowed = require('../shared/allowed');
183
+ module.exports = { nestedAccess: allowed.allowed };
184
+ `
185
+ );
186
+
187
+ const script = `
188
+ const parent = require('./parent');
189
+ bru.setVar('result', parent.nestedAccess);
190
+ `;
191
+
192
+ const context = {
193
+ bru: { setVar: jest.fn() },
194
+ console: console
195
+ };
196
+
197
+ const scriptingConfig = {
198
+ additionalContextRoots: [additionalRoot]
199
+ };
200
+
201
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
202
+
203
+ // Nested module should successfully access the additional root
204
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
205
+ });
206
+ });
207
+
208
+ describe('createCustomRequire - npm modules', () => {
209
+ it('should load npm module', async () => {
210
+ const script = `
211
+ const lodash = require('lodash');
212
+ bru.setVar('result', typeof lodash.get);
213
+ `;
214
+
215
+ const context = {
216
+ bru: { setVar: jest.fn() },
217
+ console: console
218
+ };
219
+
220
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
221
+
222
+ expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');
223
+ });
224
+ });
225
+
226
+ describe('createCustomRequire - module caching', () => {
227
+ it('should cache loaded modules', async () => {
228
+ let callCount = 0;
229
+ fs.writeFileSync(
230
+ path.join(collectionPath, 'cached.js'),
231
+ `
232
+ module.exports = { count: ${++callCount} };
233
+ `
234
+ );
235
+
236
+ const script = `
237
+ const mod1 = require('./cached');
238
+ const mod2 = require('./cached');
239
+ bru.setVar('same', mod1.count === mod2.count);
240
+ `;
241
+
242
+ const context = {
243
+ bru: { setVar: jest.fn() },
244
+ console: console
245
+ };
246
+
247
+ await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
248
+
249
+ expect(context.bru.setVar).toHaveBeenCalledWith('same', true);
250
+ });
251
+ });
252
+ });
@@ -24,7 +24,7 @@ const toNumber = (value) => {
24
24
  };
25
25
 
26
26
  const removeQuotes = (str) => {
27
- if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
27
+ if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith('\'') && str.endsWith('\''))) {
28
28
  return str.slice(1, -1);
29
29
  }
30
30
  return str;
@@ -36,7 +36,7 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
36
36
  }
37
37
  externalScript = externalScript?.trim();
38
38
 
39
- if(scriptType === 'template-literal') {
39
+ if (scriptType === 'template-literal') {
40
40
  if (!isNaN(Number(externalScript))) {
41
41
  const number = Number(externalScript);
42
42
 
@@ -341,7 +341,7 @@ const addBruShimToContext = (vm, bru) => {
341
341
  const promise = vm.newPromise();
342
342
  const dumpedUrl = vm.dump(url);
343
343
  const dumpedNameOrObj = vm.dump(nameOrCookieObj);
344
-
344
+
345
345
  // Check if the second argument is an object (cookie object case)
346
346
  if (typeof dumpedNameOrObj === 'object' && dumpedNameOrObj !== null) {
347
347
  // Cookie object case: setCookie(url, cookieObject, callback)
@@ -363,7 +363,7 @@ const addBruShimToContext = (vm, bru) => {
363
363
  }
364
364
  });
365
365
  }
366
-
366
+
367
367
  promise.settled.then(vm.runtime.executePendingJobs);
368
368
  return promise.handle;
369
369
  });
@@ -371,7 +371,7 @@ const addBruShimToContext = (vm, bru) => {
371
371
 
372
372
  const _setCookiesFn = vm.newFunction('_setCookies', (url, cookiesArray) => {
373
373
  const promise = vm.newPromise();
374
-
374
+
375
375
  nativeJar.setCookies(vm.dump(url), vm.dump(cookiesArray), (err) => {
376
376
  if (err) {
377
377
  promise.reject(marshallToVm(cleanJson(err), vm));