@usebruno/js 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -12
- package/src/bru.js +37 -15
- package/src/bruno-request.js +66 -8
- package/src/bruno-response.js +15 -2
- package/src/runtime/assert-runtime.js +5 -2
- package/src/runtime/script-runtime.js +18 -207
- package/src/runtime/test-runtime.js +12 -106
- package/src/runtime/vars-runtime.js +3 -2
- package/src/sandbox/node-vm/cjs-loader.js +369 -0
- package/src/sandbox/node-vm/constants.js +99 -0
- package/src/sandbox/node-vm/index.js +58 -188
- package/src/sandbox/node-vm/index.spec.js +1198 -0
- package/src/sandbox/node-vm/utils.js +42 -0
- package/src/sandbox/quickjs/index.js +2 -2
- package/src/sandbox/quickjs/shims/bru.js +9 -3
- package/src/sandbox/quickjs/shims/bruno-request.js +27 -0
- package/src/sandbox/quickjs/shims/lib/crypto-utils.js +7 -9
- package/src/sandbox/quickjs/shims/lib/crypto-utils.spec.js +6 -6
- package/src/sandbox/quickjs/shims/lib/utils.js +1 -1
- package/src/utils/results.js +4 -4
- package/src/utils.js +6 -6
|
@@ -1,47 +1,17 @@
|
|
|
1
|
-
const { NodeVM } = require('@usebruno/vm2');
|
|
2
|
-
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
3
1
|
const chai = require('chai');
|
|
4
|
-
const
|
|
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 || '
|
|
14
|
+
this.runtime = props?.runtime || 'quickjs';
|
|
45
15
|
}
|
|
46
16
|
|
|
47
17
|
async runTests(
|
|
@@ -67,27 +37,10 @@ class TestRuntime {
|
|
|
67
37
|
const promptVariables = request?.promptVariables || {};
|
|
68
38
|
const iterationDetails = request?.runnerIterationDetails || {};
|
|
69
39
|
const assertionResults = request?.assertionResults || [];
|
|
70
|
-
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);
|
|
71
42
|
const req = new BrunoRequest(request, historyLogger);
|
|
72
43
|
const res = new BrunoResponse(response);
|
|
73
|
-
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
|
74
|
-
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
|
75
|
-
const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);
|
|
76
|
-
const additionalContextRootsAbsolute = lodash
|
|
77
|
-
.chain(additionalContextRoots)
|
|
78
|
-
.map((acr) => (acr.startsWith('/') ? acr : path.join(collectionPath, acr)))
|
|
79
|
-
.value();
|
|
80
|
-
|
|
81
|
-
const whitelistedModules = {};
|
|
82
|
-
|
|
83
|
-
for (let module of moduleWhitelist) {
|
|
84
|
-
try {
|
|
85
|
-
whitelistedModules[module] = require(module);
|
|
86
|
-
} catch (e) {
|
|
87
|
-
// Ignore
|
|
88
|
-
console.warn(e);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
44
|
|
|
92
45
|
// extend bru with result getter methods
|
|
93
46
|
const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);
|
|
@@ -114,10 +67,6 @@ class TestRuntime {
|
|
|
114
67
|
jwt: jsonwebtoken
|
|
115
68
|
};
|
|
116
69
|
|
|
117
|
-
if (this.runtime === 'vm2') {
|
|
118
|
-
mixinTypedArrays(context);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
70
|
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
|
122
71
|
const customLogger = (type) => {
|
|
123
72
|
return (...args) => {
|
|
@@ -133,20 +82,14 @@ class TestRuntime {
|
|
|
133
82
|
};
|
|
134
83
|
}
|
|
135
84
|
|
|
136
|
-
if(runRequestByItemPathname) {
|
|
85
|
+
if (runRequestByItemPathname) {
|
|
137
86
|
context.bru.runRequest = runRequestByItemPathname;
|
|
138
87
|
}
|
|
139
88
|
|
|
140
89
|
let scriptError = null;
|
|
141
90
|
|
|
142
91
|
try {
|
|
143
|
-
if (this.runtime === '
|
|
144
|
-
await executeQuickJsVmAsync({
|
|
145
|
-
script: testsFile,
|
|
146
|
-
context: context,
|
|
147
|
-
collectionPath
|
|
148
|
-
});
|
|
149
|
-
} else if (this.runtime === 'nodevm') {
|
|
92
|
+
if (this.runtime === 'nodevm') {
|
|
150
93
|
await runScriptInNodeVm({
|
|
151
94
|
script: testsFile,
|
|
152
95
|
context,
|
|
@@ -154,49 +97,12 @@ class TestRuntime {
|
|
|
154
97
|
scriptingConfig
|
|
155
98
|
});
|
|
156
99
|
} else {
|
|
157
|
-
// default runtime is
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
external: true,
|
|
163
|
-
builtin: ['*'],
|
|
164
|
-
root: [collectionPath, ...additionalContextRootsAbsolute],
|
|
165
|
-
mock: {
|
|
166
|
-
// node libs
|
|
167
|
-
path,
|
|
168
|
-
stream,
|
|
169
|
-
util,
|
|
170
|
-
url,
|
|
171
|
-
http,
|
|
172
|
-
https,
|
|
173
|
-
punycode,
|
|
174
|
-
zlib,
|
|
175
|
-
// 3rd party libs
|
|
176
|
-
ajv,
|
|
177
|
-
'ajv-formats': addFormats,
|
|
178
|
-
btoa,
|
|
179
|
-
atob,
|
|
180
|
-
lodash,
|
|
181
|
-
moment,
|
|
182
|
-
uuid,
|
|
183
|
-
nanoid,
|
|
184
|
-
axios,
|
|
185
|
-
chai,
|
|
186
|
-
'node-fetch': fetch,
|
|
187
|
-
'crypto-js': CryptoJS,
|
|
188
|
-
'xml2js': xml2js,
|
|
189
|
-
cheerio,
|
|
190
|
-
tv4,
|
|
191
|
-
'jsonwebtoken': jsonwebtoken,
|
|
192
|
-
...whitelistedModules,
|
|
193
|
-
fs: allowScriptFilesystemAccess ? fs : undefined,
|
|
194
|
-
'node-vault': NodeVault
|
|
195
|
-
}
|
|
196
|
-
}
|
|
100
|
+
// default runtime is `quickjs`
|
|
101
|
+
await executeQuickJsVmAsync({
|
|
102
|
+
script: testsFile,
|
|
103
|
+
context: context,
|
|
104
|
+
collectionPath
|
|
197
105
|
});
|
|
198
|
-
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
|
|
199
|
-
await asyncVM();
|
|
200
106
|
}
|
|
201
107
|
} catch (error) {
|
|
202
108
|
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 || '
|
|
23
|
+
this.runtime = props?.runtime || 'quickjs';
|
|
24
24
|
this.mode = props?.mode || 'developer';
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for the Node.js VM sandbox.
|
|
3
|
+
*
|
|
4
|
+
* ECMAScript built-ins (Object, Array, Function, etc.)
|
|
5
|
+
* are NOT passed from the host. The VM provides its own versions, ensuring
|
|
6
|
+
* consistent prototype chains for libraries that use introspection.
|
|
7
|
+
*
|
|
8
|
+
* Handled separately in index.js:
|
|
9
|
+
* - global/globalThis: Points to isolated context (not host)
|
|
10
|
+
* - require: createCustomRequire() (custom module loader)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Safe globals to pass from host to VM context.
|
|
15
|
+
*
|
|
16
|
+
* ECMAScript built-ins (Object, Array, Function, String, Number,
|
|
17
|
+
* Boolean, Symbol, Date, RegExp, Map, Set, Promise, JSON, Math,
|
|
18
|
+
* parseInt, etc.) are intentionally NOT included here.
|
|
19
|
+
*
|
|
20
|
+
* The VM context provides its own versions of these, which ensures consistent
|
|
21
|
+
* prototype chains. Passing host versions causes prototype mismatches.
|
|
22
|
+
*
|
|
23
|
+
* Only Node.js-specific and Web APIs that the VM doesn't provide are listed.
|
|
24
|
+
*/
|
|
25
|
+
const safeGlobals = [
|
|
26
|
+
'process',
|
|
27
|
+
|
|
28
|
+
// Node.js timers (not part of ECMAScript)
|
|
29
|
+
'setTimeout',
|
|
30
|
+
'setInterval',
|
|
31
|
+
'clearTimeout',
|
|
32
|
+
'clearInterval',
|
|
33
|
+
'setImmediate',
|
|
34
|
+
'clearImmediate',
|
|
35
|
+
'queueMicrotask',
|
|
36
|
+
|
|
37
|
+
// Node.js globals
|
|
38
|
+
'Buffer',
|
|
39
|
+
|
|
40
|
+
// Error types - needed for instanceof checks with errors from host APIs/modules
|
|
41
|
+
'Error',
|
|
42
|
+
'TypeError',
|
|
43
|
+
'ReferenceError',
|
|
44
|
+
'SyntaxError',
|
|
45
|
+
'RangeError',
|
|
46
|
+
'URIError',
|
|
47
|
+
'EvalError',
|
|
48
|
+
'AggregateError',
|
|
49
|
+
|
|
50
|
+
// URL APIs (WHATWG - not ECMAScript)
|
|
51
|
+
'URL',
|
|
52
|
+
'URLSearchParams',
|
|
53
|
+
|
|
54
|
+
// Encoding APIs
|
|
55
|
+
'TextEncoder',
|
|
56
|
+
'TextDecoder',
|
|
57
|
+
'atob',
|
|
58
|
+
'btoa',
|
|
59
|
+
|
|
60
|
+
// Fetch API (Node 18+)
|
|
61
|
+
'fetch',
|
|
62
|
+
'Request',
|
|
63
|
+
'Response',
|
|
64
|
+
'Headers',
|
|
65
|
+
'FormData',
|
|
66
|
+
'AbortController',
|
|
67
|
+
'AbortSignal',
|
|
68
|
+
'Blob',
|
|
69
|
+
|
|
70
|
+
// Streams API
|
|
71
|
+
'ReadableStream',
|
|
72
|
+
'WritableStream',
|
|
73
|
+
'TransformStream',
|
|
74
|
+
|
|
75
|
+
// Internationalization (needs host's locale data)
|
|
76
|
+
'Intl',
|
|
77
|
+
|
|
78
|
+
// Web Crypto API
|
|
79
|
+
'crypto',
|
|
80
|
+
|
|
81
|
+
// WebAssembly
|
|
82
|
+
'WebAssembly',
|
|
83
|
+
|
|
84
|
+
// Performance API
|
|
85
|
+
'performance',
|
|
86
|
+
|
|
87
|
+
// Events API
|
|
88
|
+
'Event',
|
|
89
|
+
'EventTarget',
|
|
90
|
+
'CustomEvent',
|
|
91
|
+
|
|
92
|
+
// Message passing
|
|
93
|
+
'MessageChannel',
|
|
94
|
+
'MessagePort'
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
safeGlobals
|
|
99
|
+
};
|