@usebruno/js 0.40.0 → 0.42.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.40.0",
3
+ "version": "0.42.0",
4
4
  "license": "MIT",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -16,7 +16,7 @@
16
16
  "prepack": "npm run test"
17
17
  },
18
18
  "dependencies": {
19
- "@usebruno/common": "0.12.1",
19
+ "@usebruno/common": "0.15.0",
20
20
  "@usebruno/query": "0.1.0",
21
21
  "ajv": "^8.12.0",
22
22
  "ajv-formats": "^2.1.1",
@@ -28,6 +28,7 @@
28
28
  "cheerio": "^1.0.0",
29
29
  "crypto-js": "^4.2.0",
30
30
  "json-query": "^2.2.2",
31
+ "jsonwebtoken": "^9.0.2",
31
32
  "lodash": "^4.17.21",
32
33
  "moment": "^2.29.4",
33
34
  "nanoid": "3.3.8",
@@ -37,6 +38,7 @@
37
38
  "quickjs-emscripten": "^0.29.2",
38
39
  "tv4": "^1.3.0",
39
40
  "uuid": "^9.0.0",
41
+ "xml-formatter": "^3.5.0",
40
42
  "xml2js": "^0.6.2"
41
43
  },
42
44
  "devDependencies": {
package/src/bru.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const { cloneDeep } = require('lodash');
2
2
  const { uuid } = require('./utils');
3
+ const xmlFormat = require('xml-formatter');
3
4
  const { interpolate: _interpolate } = require('@usebruno/common');
4
5
  const { sendRequest } = require('@usebruno/requests').scripting;
5
6
  const { jar: createCookieJar } = require('@usebruno/requests').cookies;
@@ -82,6 +83,49 @@ class Bru {
82
83
  totalIterations: iterationDetails?.totalIterations
83
84
  };
84
85
 
86
+ this.utils = {
87
+ minifyJson: (json) => {
88
+ if (json === null || json === undefined) {
89
+ throw new Error('Failed to minify');
90
+ }
91
+
92
+ if (typeof json === 'object') {
93
+ try {
94
+ return JSON.stringify(json);
95
+ } catch (err) {
96
+ throw new Error(`Failed to minify: ${err?.message || err}`);
97
+ }
98
+ }
99
+
100
+ if (typeof json === 'string') {
101
+ const trimmed = json.trim();
102
+ if (trimmed === '') return trimmed;
103
+ try {
104
+ return JSON.stringify(JSON.parse(trimmed));
105
+ } catch (err) {
106
+ throw new Error(`Failed to minify: ${err?.message || err}`);
107
+ }
108
+ }
109
+
110
+ throw new TypeError('minifyJson expects a string or object');
111
+ },
112
+
113
+ minifyXml: (xml) => {
114
+ if (xml === null || xml === undefined) {
115
+ throw new Error('Failed to minify');
116
+ }
117
+
118
+ if (typeof xml === 'string') {
119
+ try {
120
+ return xmlFormat(xml, { collapseContent: false, indentation: '', lineSeparator: '' });
121
+ } catch (err) {
122
+ throw new Error(`Failed to minify: ${err?.message || err}`);
123
+ }
124
+ }
125
+
126
+ throw new TypeError('minifyXml expects a string');
127
+ }
128
+ };
85
129
  }
86
130
 
87
131
 
@@ -205,12 +249,12 @@ class Bru {
205
249
  this.historyLogger({
206
250
  uid: uuid(),
207
251
  type: 'setVar()',
208
- data: { key, value },
252
+ data: { key, value: this.interpolate(value) },
209
253
  createdAt: new Date().toISOString()
210
254
  });
211
255
  }
212
256
 
213
- this.runtimeVariables[key] = value;
257
+ this.runtimeVariables[key] = this.interpolate(value);
214
258
  }
215
259
 
216
260
  getVar(key) {
@@ -33,7 +33,9 @@ const NodeVault = require('node-vault');
33
33
  const xml2js = require('xml2js');
34
34
  const cheerio = require('cheerio');
35
35
  const tv4 = require('tv4');
36
+ const jsonwebtoken = require('jsonwebtoken');
36
37
  const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
38
+ const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
37
39
 
38
40
  class ScriptRuntime {
39
41
  constructor(props) {
@@ -101,6 +103,10 @@ class ScriptRuntime {
101
103
  __brunoTestResults: __brunoTestResults
102
104
  };
103
105
 
106
+ if (this.runtime === 'vm2') {
107
+ mixinTypedArrays(context);
108
+ }
109
+
104
110
  if (onConsoleLog && typeof onConsoleLog === 'function') {
105
111
  const customLogger = (type) => {
106
112
  return (...args) => {
@@ -195,6 +201,7 @@ class ScriptRuntime {
195
201
  'node-fetch': fetch,
196
202
  'crypto-js': CryptoJS,
197
203
  xml2js: xml2js,
204
+ jsonwebtoken,
198
205
  cheerio,
199
206
  tv4,
200
207
  ...whitelistedModules,
@@ -281,6 +288,10 @@ class ScriptRuntime {
281
288
  __brunoTestResults: __brunoTestResults
282
289
  };
283
290
 
291
+ if (this.runtime === 'vm2') {
292
+ mixinTypedArrays(context);
293
+ }
294
+
284
295
  if (onConsoleLog && typeof onConsoleLog === 'function') {
285
296
  const customLogger = (type) => {
286
297
  return (...args) => {
@@ -374,6 +385,7 @@ class ScriptRuntime {
374
385
  'node-fetch': fetch,
375
386
  'crypto-js': CryptoJS,
376
387
  'xml2js': xml2js,
388
+ jsonwebtoken,
377
389
  cheerio,
378
390
  tv4,
379
391
  ...whitelistedModules,
@@ -35,7 +35,9 @@ const NodeVault = require('node-vault');
35
35
  const xml2js = require('xml2js');
36
36
  const cheerio = require('cheerio');
37
37
  const tv4 = require('tv4');
38
+ const jsonwebtoken = require('jsonwebtoken');
38
39
  const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
40
+ const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
39
41
 
40
42
  class TestRuntime {
41
43
  constructor(props) {
@@ -107,9 +109,14 @@ class TestRuntime {
107
109
  res,
108
110
  expect: chai.expect,
109
111
  assert: chai.assert,
110
- __brunoTestResults: __brunoTestResults
112
+ __brunoTestResults: __brunoTestResults,
113
+ jwt: jsonwebtoken
111
114
  };
112
115
 
116
+ if (this.runtime === 'vm2') {
117
+ mixinTypedArrays(context);
118
+ }
119
+
113
120
  if (onConsoleLog && typeof onConsoleLog === 'function') {
114
121
  const customLogger = (type) => {
115
122
  return (...args) => {
@@ -180,6 +187,7 @@ class TestRuntime {
180
187
  'xml2js': xml2js,
181
188
  cheerio,
182
189
  tv4,
190
+ 'jsonwebtoken': jsonwebtoken,
183
191
  ...whitelistedModules,
184
192
  fs: allowScriptFilesystemAccess ? fs : undefined,
185
193
  'node-vault': NodeVault
@@ -0,0 +1,15 @@
1
+ exports.mixinTypedArrays = (obj) => {
2
+ Object.assign(obj, {
3
+ Int8Array: Int8Array,
4
+ Uint8Array: Uint8Array,
5
+ Uint8ClampedArray: Uint8ClampedArray,
6
+ Int16Array: Int16Array,
7
+ Uint16Array: Uint16Array,
8
+ Int32Array: Int32Array,
9
+ Uint32Array: Uint32Array,
10
+ Float32Array: Float32Array,
11
+ Float64Array: Float64Array,
12
+ BigInt64Array: BigInt64Array,
13
+ BigUint64Array: BigUint64Array
14
+ });
15
+ };
@@ -4,6 +4,7 @@ const path = require('node:path');
4
4
  const { get } = require('lodash');
5
5
  const lodash = require('lodash');
6
6
  const { cleanJson } = require('../../utils');
7
+ const { mixinTypedArrays } = require('../mixins/typed-arrays');
7
8
 
8
9
  class ScriptError extends Error {
9
10
  constructor(error, script) {
@@ -60,6 +61,8 @@ async function runScriptInNodeVm({
60
61
  clearImmediate: global.clearImmediate
61
62
  };
62
63
 
64
+ mixinTypedArrays(scriptContext);
65
+
63
66
  // Create shared cache for local modules
64
67
  const localModuleCache = new Map();
65
68
 
@@ -87,14 +87,15 @@ const addBrunoRequestShimToContext = (vm, req) => {
87
87
  vm.setProp(reqObject, 'setHeader', setHeader);
88
88
  setHeader.dispose();
89
89
 
90
- let getBody = vm.newFunction('getBody', function () {
91
- return marshallToVm(req.getBody(), vm);
90
+ let getBody = vm.newFunction('getBody', function (options = {}) {
91
+ return marshallToVm(req.getBody(vm.dump(options)), vm);
92
92
  });
93
+
93
94
  vm.setProp(reqObject, 'getBody', getBody);
94
95
  getBody.dispose();
95
96
 
96
- let setBody = vm.newFunction('setBody', function (data) {
97
- req.setBody(vm.dump(data));
97
+ let setBody = vm.newFunction('setBody', function (data, options = {}) {
98
+ req.setBody(vm.dump(data), vm.dump(options));
98
99
  });
99
100
  vm.setProp(reqObject, 'setBody', setBody);
100
101
  setBody.dispose();
@@ -2,12 +2,14 @@ const addAxiosShimToContext = require('./axios');
2
2
  const addNanoidShimToContext = require('./nanoid');
3
3
  const addPathShimToContext = require('./path');
4
4
  const addUuidShimToContext = require('./uuid');
5
+ const addJwtShimToContext = require('./jwt');
5
6
 
6
7
  const addLibraryShimsToContext = async (vm) => {
7
8
  await addNanoidShimToContext(vm);
8
9
  await addAxiosShimToContext(vm);
9
10
  await addUuidShimToContext(vm);
10
11
  await addPathShimToContext(vm);
12
+ await addJwtShimToContext(vm);
11
13
  };
12
14
 
13
15
  module.exports = addLibraryShimsToContext;
@@ -0,0 +1,181 @@
1
+ const jwt = require('jsonwebtoken');
2
+ const { marshallToVm, invokeFunction } = require('../../utils');
3
+
4
+ const addJwtShimToContext = async (vm) => {
5
+ // --- sign ---
6
+ const _jwtSign = vm.newFunction('sign', function (payload, secret, options, callback) {
7
+ const nativePayload = vm.dump(payload);
8
+ const nativeSecret = vm.dump(secret);
9
+
10
+ let nativeOptions;
11
+ let callbackHandle = callback;
12
+ const optionsType = options === undefined ? 'undefined' : vm.typeof(options);
13
+ if (optionsType === 'function') {
14
+ callbackHandle = options;
15
+ nativeOptions = undefined;
16
+ } else if (optionsType === 'object' && options !== null) {
17
+ nativeOptions = vm.dump(options);
18
+ }
19
+
20
+ // If a callback is provided
21
+ if (callbackHandle && vm.typeof(callbackHandle) === 'function') {
22
+ let tokenResult;
23
+ let hostError;
24
+ try {
25
+ tokenResult = nativeOptions
26
+ ? jwt.sign(nativePayload, nativeSecret, nativeOptions)
27
+ : jwt.sign(nativePayload, nativeSecret);
28
+ } catch (err) {
29
+ hostError = err;
30
+ }
31
+
32
+ try {
33
+ if (hostError) {
34
+ const errVm = vm.newError(hostError.message || String(hostError));
35
+ invokeFunction(vm, callbackHandle, [errVm, vm.undefined])
36
+ .catch((e) => {
37
+ console.warn('[JWT SHIM][sign.cb] callback invocation error:', e);
38
+ })
39
+ .finally(() => {
40
+ errVm.dispose();
41
+ callbackHandle.dispose();
42
+ });
43
+ } else {
44
+ const tokenVm = marshallToVm(String(tokenResult), vm);
45
+ invokeFunction(vm, callbackHandle, [vm.null, tokenVm])
46
+ .catch((e) => {
47
+ console.warn('[JWT SHIM][sign.cb] callback invocation error:', e);
48
+ })
49
+ .finally(() => {
50
+ tokenVm.dispose();
51
+ callbackHandle.dispose();
52
+ });
53
+ }
54
+ } catch (e) {
55
+ console.warn('[JWT SHIM][sign.cb] unexpected error:', e);
56
+ callbackHandle.dispose();
57
+ }
58
+
59
+ return vm.undefined;
60
+ }
61
+
62
+ try {
63
+ const token = nativeOptions
64
+ ? jwt.sign(nativePayload, nativeSecret, nativeOptions)
65
+ : jwt.sign(nativePayload, nativeSecret);
66
+ return marshallToVm(token, vm);
67
+ } catch (err) {
68
+ throw vm.newError(err.message || String(err));
69
+ }
70
+ });
71
+
72
+ vm.setProp(vm.global, '__bruno__jwt__sign', _jwtSign);
73
+ _jwtSign.dispose();
74
+
75
+ // --- verify ---
76
+ const _jwtVerify = vm.newFunction('verify', function (token, secret, options, callback) {
77
+ const nativeToken = vm.dump(token);
78
+ const nativeSecret = vm.dump(secret);
79
+
80
+ let nativeOptions;
81
+ let actualCallback = callback;
82
+
83
+ const optionsType = options === undefined ? 'undefined' : vm.typeof(options);
84
+ if (optionsType === 'function') {
85
+ actualCallback = options;
86
+ nativeOptions = undefined;
87
+ } else if (optionsType === 'object' && options !== null) {
88
+ nativeOptions = vm.dump(options);
89
+ }
90
+
91
+ if (actualCallback && vm.typeof(actualCallback) === 'function') {
92
+ let decodedResult;
93
+ let hostError;
94
+ try {
95
+ decodedResult = nativeOptions
96
+ ? jwt.verify(nativeToken, nativeSecret, nativeOptions)
97
+ : jwt.verify(nativeToken, nativeSecret);
98
+ } catch (err) {
99
+ hostError = err;
100
+ }
101
+
102
+ try {
103
+ if (hostError) {
104
+ const vmErr = vm.newError(hostError.message || String(hostError));
105
+ invokeFunction(vm, actualCallback, [vmErr, vm.undefined])
106
+ .catch((e) => {
107
+ console.warn('[JWT SHIM][verify.cb] callback invocation error:', e);
108
+ })
109
+ .finally(() => {
110
+ vmErr.dispose();
111
+ actualCallback.dispose();
112
+ });
113
+ } else {
114
+ const vmNull = vm.null;
115
+ const vmDecoded = marshallToVm(decodedResult, vm);
116
+ invokeFunction(vm, actualCallback, [vmNull, vmDecoded])
117
+ .catch((e) => {
118
+ console.warn('[JWT SHIM][verify.cb] callback invocation error:', e);
119
+ })
120
+ .finally(() => {
121
+ vmDecoded.dispose();
122
+ actualCallback.dispose();
123
+ });
124
+ }
125
+ } catch (e) {
126
+ console.warn('[JWT SHIM][verify.cb] unexpected error:', e);
127
+ actualCallback.dispose();
128
+ }
129
+
130
+ return vm.undefined;
131
+ }
132
+
133
+ try {
134
+ const decoded = nativeOptions
135
+ ? jwt.verify(nativeToken, nativeSecret, nativeOptions)
136
+ : jwt.verify(nativeToken, nativeSecret);
137
+ return marshallToVm(decoded, vm);
138
+ } catch (err) {
139
+ throw vm.newError(err.message || String(err));
140
+ }
141
+ });
142
+
143
+ vm.setProp(vm.global, '__bruno__jwt__verify', _jwtVerify);
144
+ _jwtVerify.dispose();
145
+
146
+ // --- decode ---
147
+ const _jwtDecode = vm.newFunction('decode', function (token, options) {
148
+ const nativeToken = vm.dump(token);
149
+
150
+ let nativeOptions;
151
+ const optionsType = options === undefined ? 'undefined' : vm.typeof(options);
152
+ if (optionsType === 'object' && options !== null) {
153
+ nativeOptions = vm.dump(options);
154
+ }
155
+
156
+ try {
157
+ const decoded = nativeOptions
158
+ ? jwt.decode(nativeToken, nativeOptions)
159
+ : jwt.decode(nativeToken);
160
+ return marshallToVm(decoded, vm);
161
+ } catch (err) {
162
+ throw vm.newError(err.message || String(err));
163
+ }
164
+ });
165
+
166
+ vm.setProp(vm.global, '__bruno__jwt__decode', _jwtDecode);
167
+ _jwtDecode.dispose();
168
+
169
+ vm.evalCode(`
170
+ globalThis.jwt = {};
171
+ globalThis.jwt.sign = globalThis.__bruno__jwt__sign;
172
+ globalThis.jwt.verify = globalThis.__bruno__jwt__verify;
173
+ globalThis.jwt.decode = globalThis.__bruno__jwt__decode;
174
+ globalThis.requireObject = {
175
+ ...globalThis.requireObject,
176
+ 'jsonwebtoken': globalThis.jwt,
177
+ };
178
+ `);
179
+ };
180
+
181
+ module.exports = addJwtShimToContext;
@@ -30,6 +30,54 @@ const marshallToVm = (value, vm) => {
30
30
  }
31
31
  };
32
32
 
33
+ /**
34
+ * Invokes a QuickJS function handle.
35
+ * - Returns a Promise
36
+ *
37
+ * @param {Object} vm - QuickJS VM instance
38
+ * @param {QuickJSHandle} quickFn - A QuickJS function handle
39
+ * @param {Array} args - Arguments to pass to the function
40
+ * @returns {Promise<any>} - The result as a Promise
41
+ */
42
+ async function invokeFunction(vm, quickFn, args = []) {
43
+ if (vm.typeof(quickFn) !== 'function') {
44
+ throw new TypeError('Target is not a QuickJS function');
45
+ }
46
+
47
+ const result = vm.callFunction(quickFn, vm.global, ...args);
48
+
49
+ if (result.error) {
50
+ const error = vm.dump(result.error);
51
+ result.error.dispose();
52
+ throw error;
53
+ }
54
+
55
+ // Check if the result is a QuickJS Promise handle (async functions)
56
+ if (vm.typeof(result.value) === 'object' && result.value.constructor && vm.typeof(result.value.constructor) === 'function') {
57
+ try {
58
+ const promiseHandle = vm.unwrapResult(result);
59
+ const resolvedResult = await vm.resolvePromise(promiseHandle);
60
+ promiseHandle.dispose();
61
+ const resolvedHandle = vm.unwrapResult(resolvedResult);
62
+ const value = vm.dump(resolvedHandle);
63
+ resolvedHandle.dispose();
64
+ return Promise.resolve(value);
65
+ } catch (promiseError) {
66
+ // If it's not a valid Promise, throw an error
67
+ result.value.dispose();
68
+ throw new Error(`Invalid Promise handle: ${promiseError.message}`);
69
+ }
70
+ }
71
+
72
+ const value = vm.dump(result.value);
73
+ result.value.dispose();
74
+
75
+ return (value && typeof value.then === 'function')
76
+ ? value
77
+ : Promise.resolve(value);
78
+ }
79
+
33
80
  module.exports = {
34
- marshallToVm
81
+ marshallToVm,
82
+ invokeFunction
35
83
  };
package/src/utils.js CHANGED
@@ -137,10 +137,58 @@ const createResponseParser = (response = {}) => {
137
137
  * Remove the cleanJson fix and execute the below post response script
138
138
  * bru.setVar("a", {b:3});
139
139
  * Todo: Find a better fix
140
+ *
141
+ * serializes typedArrays by using Buffer to handle most binary cases
142
+ * // TODO: reaper, replace with `devalue` after evaluating all cases, current setup is
143
+ * more of a hotfix
140
144
  */
141
145
  const cleanJson = (data) => {
146
+ const typedArrays = [
147
+ // Baseline typed arrays
148
+ Int8Array,
149
+ Uint8Array,
150
+ Uint8ClampedArray,
151
+ Int16Array,
152
+ Uint16Array,
153
+ Int32Array,
154
+ Uint32Array,
155
+ Float32Array,
156
+ Float64Array,
157
+ BigInt64Array,
158
+ BigUint64Array,
159
+
160
+ // Baseline 2025 Newly available
161
+ 'Float16Array' in globalThis ? globalThis['Float16Array'] : null
162
+ ].filter(Boolean);
163
+ const binaryNames = typedArrays.map((d) => d.name);
164
+
165
+ const replacer = (key, value) => {
166
+ const isBinary = typedArrays.find((d) => value instanceof d);
167
+ if (isBinary) {
168
+ return {
169
+ __cleanJSONType: isBinary.name,
170
+ __cleanJSONValue: Buffer.from(value.buffer).toJSON()
171
+ };
172
+ }
173
+ return value;
174
+ };
175
+
176
+ const reviver = (key, value) => {
177
+ if (typeof value !== 'object' || value === null) {
178
+ return value;
179
+ }
180
+ if ('__cleanJSONType' in value && '__cleanJSONValue' in value) {
181
+ const matchedName = binaryNames.find((d) => value.__cleanJSONType === d);
182
+ if (!matchedName) return value;
183
+ const binConstructor = typedArrays.find((d) => d.name === matchedName);
184
+
185
+ return binConstructor.from(Buffer.from(value.__cleanJSONValue));
186
+ }
187
+ return value;
188
+ };
189
+
142
190
  try {
143
- return JSON.parse(JSON.stringify(data));
191
+ return JSON.parse(JSON.stringify(data, replacer), reviver);
144
192
  } catch (e) {
145
193
  return data;
146
194
  }