@usebruno/js 0.46.1 → 0.48.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.46.1",
3
+ "version": "0.48.0",
4
4
  "license": "MIT",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -13,8 +13,8 @@
13
13
  "prepack": "npm run test"
14
14
  },
15
15
  "dependencies": {
16
- "@usebruno/common": "0.20.0",
17
- "@usebruno/query": "0.2.0",
16
+ "@usebruno/common": "0.22.0",
17
+ "@usebruno/query": "0.2.2",
18
18
  "ajv": "^8.12.0",
19
19
  "ajv-formats": "^2.1.1",
20
20
  "atob": "^2.1.2",
@@ -24,6 +24,7 @@
24
24
  "chai-string": "^1.5.0",
25
25
  "cheerio": "^1.0.0",
26
26
  "crypto-js": "^4.2.0",
27
+ "handlebars": "^4.7.9",
27
28
  "json-query": "^2.2.2",
28
29
  "jsonwebtoken": "^9.0.3",
29
30
  "lodash": "^4.17.21",
@@ -31,17 +32,18 @@
31
32
  "nanoid": "3.3.8",
32
33
  "node-fetch": "^2.7.0",
33
34
  "path": "^0.12.7",
34
- "quickjs-emscripten": "^0.29.2",
35
+ "quickjs-emscripten": "^0.32.0",
35
36
  "tv4": "^1.3.0",
36
- "uuid": "^9.0.0",
37
+ "uuid": "^10.0.0",
37
38
  "xml-formatter": "^3.5.0",
38
39
  "xml2js": "^0.6.2",
39
40
  "yaml": "^2.3.4"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@rollup/plugin-commonjs": "^23.0.2",
44
+ "@rollup/plugin-json": "^6.0.0",
43
45
  "@rollup/plugin-node-resolve": "^15.0.1",
44
- "rollup": "3.29.5",
45
- "rollup-plugin-terser": "^7.0.2"
46
+ "@rollup/plugin-terser": "^1.0.0",
47
+ "rollup": "3.30.0"
46
48
  }
47
49
  }
package/src/bru.js CHANGED
@@ -3,31 +3,90 @@ const { uuid } = require('./utils');
3
3
  const xmlFormat = require('xml-formatter');
4
4
  const { interpolate: _interpolate } = require('@usebruno/common');
5
5
  const { sendRequest, createSendRequest } = require('@usebruno/requests').scripting;
6
- const { jar: createCookieJar } = require('@usebruno/requests').cookies;
7
-
6
+ const { jar: createCookieJar, getCookiesForUrl } = require('@usebruno/requests').cookies;
7
+ const CookieList = require('./cookie-list');
8
+ const Handlebars = require('handlebars');
9
+
10
+ const VISUALIZATION_CLEAR = '__clear__';
11
+ const MAX_TEMPLATE_LENGTH = 1_000_000; // 1 million characters
12
+ /*
13
+ * All Handlebars.compile() options (v4.x):
14
+ * data - set false to disable @data tracking
15
+ * compat - enable recursive field lookup
16
+ * knownHelpers - hash of helpers known at compile time (optimization)
17
+ * knownHelpersOnly - only allow known helpers (optimization) [SAFE]
18
+ * noEscape - skip HTML escaping of content [SAFE]
19
+ * strict - throw on missing fields instead of ignoring [SAFE]
20
+ * assumeObjects - skip object existence checks on paths [SAFE]
21
+ * preventIndent - disable auto-indent for partials [SAFE]
22
+ * ignoreStandalone - disable standalone tag removal [SAFE]
23
+ * explicitPartialContext - disable implicit context for partials [SAFE]
24
+ *
25
+ * Excluded from allowlist:
26
+ * data, compat, knownHelpers - can alter template resolution in unexpected ways
27
+ * allowProtoPropertiesByDefault, allowProtoMethodsByDefault - defense-in-depth against
28
+ * prototype traversal. Handlebars >=4.3.0 fixed the original CVE-2019-19919 and >=4.7.0
29
+ * hardcodes blocks on __proto__, constructor, etc. even when these flags are true, but
30
+ * we still exclude them to avoid relaxing the security posture.
31
+ * https://nvd.nist.gov/vuln/detail/CVE-2019-19919
32
+ *
33
+ * Use `dangerouslyAllowAllOptions: true` in data.options to bypass the allowlist.
34
+ */
35
+ const SAFE_HBS_OPTIONS = new Set([
36
+ 'noEscape', 'strict', 'assumeObjects', 'preventIndent',
37
+ 'ignoreStandalone', 'explicitPartialContext', 'knownHelpersOnly'
38
+ ]);
8
39
  const variableNameRegex = /^[\w-.]*$/;
9
40
 
10
41
  class Bru {
11
42
  /**
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)
43
+ * @param {object} options - Single options object (destructured)
44
+ * @property {string} options.runtime - The runtime environment ('quickjs' or 'nodevm')
45
+ * @property {object} [options.envVariables={}] - Environment variables
46
+ * @property {object} [options.runtimeVariables={}] - Runtime variables
47
+ * @property {object} [options.processEnvVars={}] - Process environment variables (deep cloned)
48
+ * @property {string} [options.collectionPath] - Path to the collection
49
+ * @property {function} [options.historyLogger] - History logger function
50
+ * @property {function} [options.setVisualizations] - Visualizations setter function
51
+ * @property {object} [options.secretVariables={}] - Secret variables (deep cloned)
52
+ * @property {object} [options.collectionVariables={}] - Collection-level variables
53
+ * @property {object} [options.folderVariables={}] - Folder-level variables
54
+ * @property {object} [options.requestVariables={}] - Request-level variables
55
+ * @property {object} [options.globalEnvironmentVariables={}] - Global environment variables
56
+ * @property {object} [options.oauth2CredentialVariables={}] - OAuth2 credential variables
57
+ * @property {object} [options.iterationDetails] - Iteration details for runner
58
+ * @property {string} [options.collectionName] - Name of the collection
59
+ * @property {object} [options.promptVariables={}] - Prompt variables
60
+ * @property {object} [options.certsAndProxyConfig] - Configuration for bru.sendRequest (proxy, certs, TLS)
61
+ * @property {string} [options.certsAndProxyConfig.collectionPath] - Path to the collection
62
+ * @property {object} [options.certsAndProxyConfig.options] - TLS and proxy options
63
+ * @property {object} [options.certsAndProxyConfig.clientCertificates] - Client certificate configuration
64
+ * @property {object} [options.certsAndProxyConfig.collectionLevelProxy] - Collection-level proxy settings
65
+ * @property {object} [options.certsAndProxyConfig.systemProxyConfig] - System proxy configuration
66
+ * @property {string} [options.requestUrl] - The URL of the current request (used for cookie access)
67
+ * @property {function} [options.onConsoleLog] - Console callback `(type, args) => void` to route warnings to the UI
29
68
  */
30
- constructor(runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, historyLogger, setVisualizations, secretVariables, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, iterationDetails, collectionName, promptVariables, certsAndProxyConfig) {
69
+ constructor({
70
+ runtime,
71
+ envVariables,
72
+ runtimeVariables,
73
+ processEnvVars,
74
+ collectionPath,
75
+ historyLogger,
76
+ setVisualizations,
77
+ secretVariables,
78
+ collectionVariables,
79
+ folderVariables,
80
+ requestVariables,
81
+ globalEnvironmentVariables,
82
+ oauth2CredentialVariables,
83
+ iterationDetails,
84
+ collectionName,
85
+ promptVariables,
86
+ certsAndProxyConfig,
87
+ requestUrl,
88
+ onConsoleLog
89
+ }) {
31
90
  this.envVariables = envVariables || {};
32
91
  this.runtimeVariables = runtimeVariables || {};
33
92
  this.promptVariables = promptVariables || {};
@@ -41,58 +100,18 @@ class Bru {
41
100
  this.collectionPath = collectionPath;
42
101
  this.historyLogger = historyLogger;
43
102
  this.setVisualizations = setVisualizations;
103
+ this.onConsoleLog = onConsoleLog || null;
44
104
  this.collectionName = collectionName;
45
105
  // Use createSendRequest with config if provided, otherwise use default sendRequest
46
106
  this.sendRequest = certsAndProxyConfig ? createSendRequest(certsAndProxyConfig) : sendRequest;
47
107
  this.runtime = runtime;
48
- this.cookies = {
49
- jar: () => {
50
- const cookieJar = createCookieJar();
51
-
52
- return {
53
- getCookie: (url, cookieName, callback) => {
54
- const interpolatedUrl = this.interpolate(url);
55
- return cookieJar.getCookie(interpolatedUrl, cookieName, callback);
56
- },
57
-
58
- getCookies: (url, callback) => {
59
- const interpolatedUrl = this.interpolate(url);
60
- return cookieJar.getCookies(interpolatedUrl, callback);
61
- },
62
-
63
- setCookie: (url, nameOrCookieObj, valueOrCallback, maybeCallback) => {
64
- const interpolatedUrl = this.interpolate(url);
65
- return cookieJar.setCookie(interpolatedUrl, nameOrCookieObj, valueOrCallback, maybeCallback);
66
- },
67
-
68
- setCookies: (url, cookiesArray, callback) => {
69
- const interpolatedUrl = this.interpolate(url);
70
- return cookieJar.setCookies(interpolatedUrl, cookiesArray, callback);
71
- },
72
-
73
- // Clear entire cookie jar
74
- clear: (callback) => {
75
- return cookieJar.clear(callback);
76
- },
77
-
78
- // Delete cookies for a specific URL/domain
79
- deleteCookies: (url, callback) => {
80
- const interpolatedUrl = this.interpolate(url);
81
- return cookieJar.deleteCookies(interpolatedUrl, callback);
82
- },
83
-
84
- deleteCookie: (url, cookieName, callback) => {
85
- const interpolatedUrl = this.interpolate(url);
86
- return cookieJar.deleteCookie(interpolatedUrl, cookieName, callback);
87
- },
88
-
89
- hasCookie: (url, cookieName, callback) => {
90
- const interpolatedUrl = this.interpolate(url);
91
- return cookieJar.hasCookie(interpolatedUrl, cookieName, callback);
92
- }
93
- };
94
- }
95
- };
108
+ this.requestUrl = requestUrl;
109
+ this.cookies = new CookieList({
110
+ getUrl: () => this.interpolate(this.requestUrl),
111
+ interpolate: (str) => this.interpolate(str),
112
+ createCookieJar,
113
+ getCookiesForUrl
114
+ });
96
115
  // Holds variables that are marked as persistent by scripts
97
116
  this.persistentEnvVariables = {};
98
117
  // Holds credential IDs to be reset after script execution
@@ -414,9 +433,41 @@ class Bru {
414
433
  return this.secretVariables?.[`$secrets.${key}`];
415
434
  }
416
435
 
436
+ /**
437
+ * Render a visualization in the response pane.
438
+ *
439
+ * @param {'table' | 'html'} type - The visualization type.
440
+ * @param {Object} data - Shape depends on `type`:
441
+ *
442
+ * **When `type` is `'table'`:**
443
+ * @param {string} data.provider - `'ag-grid'` or `'react-table'`.
444
+ * @param {Object} data.props - Props forwarded to the grid component.
445
+ * @param {Array} data.props.columnDefinitions - (ag-grid) Column definitions.
446
+ * @param {Array} data.props.rowData - (ag-grid) Row data.
447
+ *
448
+ * **When `type` is `'html'`** (raw mode):
449
+ * @param {string} data.content - Pre-rendered HTML string.
450
+ *
451
+ * **When `type` is `'html'`** (Handlebars template mode):
452
+ * @param {string} data.template - Handlebars template string (required).
453
+ * @param {Object} [data.data={}] - Context object passed to the compiled template.
454
+ * @param {Object} [data.options] - Handlebars compile options.
455
+ * @param {boolean} [data.options.noEscape] - Disable HTML escaping.
456
+ * @param {boolean} [data.options.strict] - Throw on missing variables.
457
+ * @param {boolean} [data.options.assumeObjects] - Skip null checks on nested paths.
458
+ * @param {boolean} [data.options.dangerouslyAllowAllOptions] - Bypass the safe-options
459
+ * allowlist (forwards all remaining options directly to `Handlebars.compile`).
460
+ *
461
+ * @throws {Error} If `type` is falsy.
462
+ * @throws {Error} If `type` is `'html'` and neither `data.content` nor `data.template` is provided.
463
+ * @throws {Error} If template exceeds `MAX_TEMPLATE_LENGTH`.
464
+ */
417
465
  visualize(type, data) {
418
- if (type == 'table') {
419
- if (data?.provider == 'ag-grid') {
466
+ if (!type) {
467
+ throw new Error('visualization type is required');
468
+ }
469
+ if (type === 'table') {
470
+ if (data?.provider === 'ag-grid') {
420
471
  if (!data?.props?.columnDefinitions) {
421
472
  throw new Error(`columns definitions are required`);
422
473
  }
@@ -424,17 +475,63 @@ class Bru {
424
475
  throw new Error(`row data is required`);
425
476
  }
426
477
  this.setVisualizations({ uid: uuid(), type, data });
427
- } else if (data?.provider == 'react-table') {
478
+ } else if (data?.provider === 'react-table') {
428
479
  this.setVisualizations({ uid: uuid(), type, data });
429
480
  }
430
- } else if (type == 'html') {
431
- if (!data?.content) {
432
- throw new Error(`html content is required`);
481
+ } else if (type === 'html') {
482
+ if (typeof data?.template === 'string') {
483
+ // Handlebars template mode (translated from pm.visualizer.set)
484
+ if (data.template.length > MAX_TEMPLATE_LENGTH) {
485
+ throw new Error(`template exceeds maximum length of ${MAX_TEMPLATE_LENGTH} characters`);
486
+ }
487
+ // Isolated instance so globally-registered helpers can't be exploited (SSTI prevention)
488
+ const hbs = Handlebars.create();
489
+ // Allowlist safe compile options to prevent re-enabling prototype traversal (CVE-2019-19919)
490
+ let compileOptions;
491
+ if (data.options?.dangerouslyAllowAllOptions) {
492
+ const { dangerouslyAllowAllOptions, ...rest } = data.options;
493
+ compileOptions = rest;
494
+ } else {
495
+ compileOptions = {};
496
+ if (data.options) {
497
+ const blocked = [];
498
+ for (const key of Object.keys(data.options)) {
499
+ if (SAFE_HBS_OPTIONS.has(key)) {
500
+ compileOptions[key] = data.options[key];
501
+ } else {
502
+ blocked.push(key);
503
+ }
504
+ }
505
+ if (blocked.length > 0) {
506
+ const msg = `bru.visualize: the following compile options were blocked and ignored: ${blocked.map((k) => `"${k}"`).join(', ')}. `
507
+ + 'Use { dangerouslyAllowAllOptions: true } in options to bypass the allowlist.';
508
+ if (this.onConsoleLog) {
509
+ this.onConsoleLog('warn', [msg]);
510
+ } else {
511
+ console.warn(msg);
512
+ }
513
+ }
514
+ }
515
+ }
516
+ const compiled = hbs.compile(data.template, compileOptions);
517
+ const renderedHtml = compiled(data.data || {});
518
+ this.setVisualizations({
519
+ uid: uuid(), type,
520
+ data: { content: renderedHtml, rawData: data.data || {} }
521
+ });
522
+ } else if (data?.content) {
523
+ // Existing raw HTML mode
524
+ this.setVisualizations({ uid: uuid(), type, data });
525
+ } else {
526
+ throw new Error('html content or template is required');
433
527
  }
434
- this.setVisualizations({ uid: uuid(), type, data });
435
528
  }
436
529
  }
437
530
 
531
+ clearVisualizations() {
532
+ this.setVisualizations({ type: VISUALIZATION_CLEAR });
533
+ }
534
+
438
535
  sleep(ms) {
439
536
  return new Promise((resolve) => setTimeout(resolve, ms));
440
537
  }
@@ -488,3 +585,4 @@ class IterationDataManager {
488
585
  }
489
586
 
490
587
  module.exports = Bru;
588
+ module.exports.VISUALIZATION_CLEAR = VISUALIZATION_CLEAR;
@@ -1,9 +1,12 @@
1
+ const HeaderList = require('./header-list');
2
+
1
3
  class BrunoRequest {
2
4
  /**
3
5
  * The following properties are available as shorthand:
4
6
  * - req.url
5
7
  * - req.method
6
- * - req.headers
8
+ * - req.headers (raw headers object)
9
+ * - req.headerList (PropertyList API for headers)
7
10
  * - req.timeout
8
11
  * - req.body
9
12
  *
@@ -22,6 +25,7 @@ class BrunoRequest {
22
25
  this.name = req.name;
23
26
  this.pathParams = req.pathParams;
24
27
  this.tags = req.tags || [];
28
+ this.headerList = new HeaderList(this.req);
25
29
  /**
26
30
  * We automatically parse the JSON body if the content type is JSON
27
31
  * This is to make it easier for the user to access the body directly
@@ -96,17 +100,24 @@ class BrunoRequest {
96
100
  }
97
101
 
98
102
  getAuthMode() {
103
+ const headers = this.req.headers;
99
104
  if (this.req?.oauth2) {
100
105
  return 'oauth2';
101
- } else if (this.headers?.['Authorization']?.startsWith('Bearer')) {
106
+ } else if (this.req?.oauth1config) {
107
+ return 'oauth1';
108
+ } else if (headers?.['Authorization']?.startsWith('Bearer')) {
102
109
  return 'bearer';
103
- } else if (this.headers?.['Authorization']?.startsWith('Basic') || this.req?.auth?.username) {
110
+ } else if (headers?.['Authorization']?.startsWith('Basic') || this.req?.auth?.username) {
104
111
  return 'basic';
112
+ } else if (this.req?.apiKeyAuthValueForQueryParams) {
113
+ return 'apikey';
114
+ } else if (this.req?.apiKeyHeaderName && this.headers?.[this.req.apiKeyHeaderName] !== undefined) {
115
+ return 'apikey';
105
116
  } else if (this.req?.awsv4) {
106
117
  return 'awsv4';
107
118
  } else if (this.req?.digestConfig) {
108
119
  return 'digest';
109
- } else if (this.headers?.['X-WSSE'] || this.req?.auth?.username) {
120
+ } else if (headers?.['X-WSSE'] || this.req?.auth?.username) {
110
121
  return 'wsse';
111
122
  } else {
112
123
  return 'none';
@@ -123,7 +134,6 @@ class BrunoRequest {
123
134
  }
124
135
 
125
136
  setHeaders(headers) {
126
- this.headers = headers;
127
137
  this.req.headers = headers;
128
138
  }
129
139
 
@@ -136,12 +146,10 @@ class BrunoRequest {
136
146
  }
137
147
 
138
148
  setHeader(name, value) {
139
- this.headers[name] = value;
140
149
  this.req.headers[name] = value;
141
150
  }
142
151
 
143
152
  deleteHeader(name) {
144
- delete this.headers[name];
145
153
  delete this.req.headers[name];
146
154
 
147
155
  /**
@@ -1,5 +1,6 @@
1
1
  const { get } = require('@usebruno/query');
2
2
  const _ = require('lodash');
3
+ const HeaderList = require('./header-list');
3
4
 
4
5
  class BrunoResponse {
5
6
  constructor(res) {
@@ -11,6 +12,9 @@ class BrunoResponse {
11
12
  this.responseTime = res ? res.responseTime : null;
12
13
  this.url = res?.request ? res.request.protocol + '//' + res.request.host + res.request.path : null;
13
14
 
15
+ // HeaderList in static read-only mode — write methods throw
16
+ this.headerList = new HeaderList(res, { writable: false });
17
+
14
18
  // Make the instance callable
15
19
  const callable = (...args) => get(this.body, ...args);
16
20
  Object.setPrototypeOf(callable, this.constructor.prototype);