@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 +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 +4 -1
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebruno/js",
|
|
3
|
-
"version": "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.
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
274
|
+
data: { key, value: value },
|
|
254
275
|
createdAt: new Date().toISOString()
|
|
255
276
|
});
|
|
256
277
|
}
|
|
257
278
|
|
|
258
|
-
this.runtimeVariables[key] =
|
|
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 {
|
package/src/bruno-request.js
CHANGED
|
@@ -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
|
package/src/bruno-response.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
};
|