From 4badee903affef2f5250ac18883ed3f4f267d453 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Thu, 24 Jul 2025 18:48:25 +0530 Subject: [PATCH 01/30] Add @usebruno/filestore package (#5130) --- .github/workflows/tests.yml | 3 + package-lock.json | 98 +++++- package.json | 4 +- packages/bruno-cli/package.json | 1 + packages/bruno-cli/src/commands/run.js | 12 +- packages/bruno-cli/src/utils/bru.js | 18 +- packages/bruno-cli/src/utils/collection.js | 20 +- packages/bruno-electron/package.json | 1 + .../src/app/collection-watcher.js | 38 ++- packages/bruno-electron/src/bru/index.js | 279 ------------------ .../bruno-electron/src/bru/workers/index.js | 64 ---- .../src/bru/workers/scripts/bru-to-json.js | 16 - .../src/bru/workers/scripts/json-to-bru.js | 16 - packages/bruno-electron/src/ipc/collection.js | 83 +++--- .../bruno-electron/src/utils/collection.js | 37 ++- packages/bruno-electron/src/workers/index.js | 68 ----- .../tests/utils/collection.spec.js | 215 ++++++++++++-- packages/bruno-filestore/.gitignore | 5 + packages/bruno-filestore/LICENSE.md | 22 ++ packages/bruno-filestore/README.md | 50 ++++ packages/bruno-filestore/babel.config.js | 6 + packages/bruno-filestore/jest.config.js | 13 + packages/bruno-filestore/package.json | 46 +++ packages/bruno-filestore/rollup.config.js | 63 ++++ .../bruno-filestore/src/formats/bru/index.ts | 203 +++++++++++++ packages/bruno-filestore/src/index.ts | 140 +++++++++ packages/bruno-filestore/src/types.ts | 141 +++++++++ .../bruno-filestore/src/types/bruno-lang.d.ts | 9 + .../src/workers/WorkerQueue/index.ts | 114 +++++++ packages/bruno-filestore/src/workers/index.ts | 86 ++++++ .../src/workers/worker-script.ts | 27 ++ packages/bruno-filestore/tsconfig.json | 22 ++ scripts/setup.js | 1 + 33 files changed, 1366 insertions(+), 555 deletions(-) delete mode 100644 packages/bruno-electron/src/bru/index.js delete mode 100644 packages/bruno-electron/src/bru/workers/index.js delete mode 100644 packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js delete mode 100644 packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js delete mode 100644 packages/bruno-electron/src/workers/index.js create mode 100644 packages/bruno-filestore/.gitignore create mode 100644 packages/bruno-filestore/LICENSE.md create mode 100644 packages/bruno-filestore/README.md create mode 100644 packages/bruno-filestore/babel.config.js create mode 100644 packages/bruno-filestore/jest.config.js create mode 100644 packages/bruno-filestore/package.json create mode 100644 packages/bruno-filestore/rollup.config.js create mode 100644 packages/bruno-filestore/src/formats/bru/index.ts create mode 100644 packages/bruno-filestore/src/index.ts create mode 100644 packages/bruno-filestore/src/types.ts create mode 100644 packages/bruno-filestore/src/types/bruno-lang.d.ts create mode 100644 packages/bruno-filestore/src/workers/WorkerQueue/index.ts create mode 100644 packages/bruno-filestore/src/workers/index.ts create mode 100644 packages/bruno-filestore/src/workers/worker-script.ts create mode 100644 packages/bruno-filestore/tsconfig.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aaf5d1880..2a829098e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,6 +30,7 @@ jobs: npm run sandbox:bundle-libraries --workspace=packages/bruno-js npm run build --workspace=packages/bruno-converters npm run build --workspace=packages/bruno-requests + npm run build --workspace=packages/bruno-filestore - name: Lint Check run: npm run lint @@ -80,6 +81,7 @@ jobs: npm run sandbox:bundle-libraries --workspace=packages/bruno-js npm run build --workspace=packages/bruno-converters npm run build --workspace=packages/bruno-requests + npm run build --workspace=packages/bruno-filestore - name: Run tests run: | @@ -125,6 +127,7 @@ jobs: npm run sandbox:bundle-libraries --workspace=packages/bruno-js npm run build:bruno-converters npm run build:bruno-requests + npm run build:bruno-filestore - name: Run Playwright tests run: | diff --git a/package-lock.json b/package-lock.json index 577b1d843..ce045a5c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "packages/bruno-tests", "packages/bruno-toml", "packages/bruno-graphql-docs", - "packages/bruno-requests" + "packages/bruno-requests", + "packages/bruno-filestore" ], "devDependencies": { "@faker-js/faker": "^7.6.0", @@ -8725,6 +8726,10 @@ "integrity": "sha512-khvEnRF6/UVDw4df06j+6lFWGNDYWlcWnxfmEgU2o/CdsGY291NC1Cexz99ud7sbGBQP2d8JUXZe4zXPkGNJpQ==", "license": "MIT" }, + "node_modules/@usebruno/filestore": { + "resolved": "packages/bruno-filestore", + "link": true + }, "node_modules/@usebruno/graphql-docs": { "resolved": "packages/bruno-graphql-docs", "link": true @@ -29922,6 +29927,7 @@ "@aws-sdk/credential-providers": "3.750.0", "@usebruno/common": "0.1.0", "@usebruno/converters": "^0.1.0", + "@usebruno/filestore": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/requests": "^0.1.0", @@ -31688,6 +31694,7 @@ "@aws-sdk/credential-providers": "3.750.0", "@usebruno/common": "0.1.0", "@usebruno/converters": "^0.1.0", + "@usebruno/filestore": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/node-machine-id": "^2.0.0", @@ -32803,6 +32810,95 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "packages/bruno-filestore": { + "name": "@usebruno/filestore", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@usebruno/lang": "0.12.0", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@babel/preset-env": "^7.22.0", + "@babel/preset-typescript": "^7.22.0", + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^9.0.2", + "@types/jest": "^29.5.11", + "@types/lodash": "^4.14.191", + "babel-jest": "^29.7.0", + "jest": "^29.2.0", + "rimraf": "^3.0.2", + "rollup": "3.29.5", + "rollup-plugin-dts": "^5.0.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-terser": "^7.0.2", + "typescript": "^4.8.4" + } + }, + "packages/bruno-filestore/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "packages/bruno-filestore/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/bruno-filestore/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "packages/bruno-filestore/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/bruno-graphql-docs": { "name": "@usebruno/graphql-docs", "version": "0.1.0", diff --git a/package.json b/package.json index 55873fff4..782d89866 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "packages/bruno-tests", "packages/bruno-toml", "packages/bruno-graphql-docs", - "packages/bruno-requests" + "packages/bruno-requests", + "packages/bruno-filestore" ], "homepage": "https://usebruno.com", "devDependencies": { @@ -48,6 +49,7 @@ "dev:electron:debug": "npm run debug --workspace=packages/bruno-electron", "build:bruno-common": "npm run build --workspace=packages/bruno-common", "build:bruno-requests": "npm run build --workspace=packages/bruno-requests", + "build:bruno-filestore": "npm run build --workspace=packages/bruno-filestore", "build:bruno-converters": "npm run build --workspace=packages/bruno-converters", "build:bruno-query": "npm run build --workspace=packages/bruno-query", "build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs", diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index e1b74e191..ca55861f1 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -53,6 +53,7 @@ "@usebruno/vm2": "^3.9.13", "@usebruno/requests": "^0.1.0", "@usebruno/converters": "^0.1.0", + "@usebruno/filestore": "^0.1.0", "aws4-axios": "^3.3.0", "axios": "^1.8.3", "axios-ntlm": "^1.4.2", diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 811314d41..1ca689fe2 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -5,15 +5,15 @@ const { forOwn, cloneDeep } = require('lodash'); const { getRunnerSummary } = require('@usebruno/common/runner'); const { exists, isFile, isDirectory } = require('../utils/filesystem'); const { runSingleRequest } = require('../runner/run-single-request'); -const { bruToEnvJson, getEnvVars } = require('../utils/bru'); +const { getEnvVars } = require('../utils/bru'); const { isRequestTagsIncluded } = require("@usebruno/common") const makeJUnitOutput = require('../reporters/junit'); const makeHtmlOutput = require('../reporters/html'); const { rpad } = require('../utils/common'); -const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru'); -const { dotenvToJson } = require('@usebruno/lang'); +const { getOptions } = require('../utils/bru'); +const { parseDotEnv, parseEnvironment } = require('@usebruno/filestore'); const constants = require('../constants'); -const { findItemInCollection, getAllRequestsInFolder, createCollectionJsonFromPathname, getCallStack } = require('../utils/collection'); +const { findItemInCollection, createCollectionJsonFromPathname, getCallStack } = require('../utils/collection'); const command = 'run [paths...]'; const desc = 'Run one or more requests/folders'; @@ -346,7 +346,7 @@ const handler = async function (argv) { } const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n'); - const envJson = bruToEnvJson(envBruContent); + const envJson = parseEnvironment(envBruContent); envVars = getEnvVars(envJson); envVars.__name__ = envFile ? path.basename(envFilePath, '.bru') : env; } @@ -439,7 +439,7 @@ const handler = async function (argv) { }; if (dotEnvExists) { const content = fs.readFileSync(dotEnvPath, 'utf8'); - const jsonData = dotenvToJson(content); + const jsonData = parseDotEnv(content); forOwn(jsonData, (value, key) => { processEnvVars[key] = value; diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js index b709f76f9..9202c0c1a 100644 --- a/packages/bruno-cli/src/utils/bru.js +++ b/packages/bruno-cli/src/utils/bru.js @@ -1,9 +1,12 @@ const _ = require('lodash'); -const { bruToEnvJsonV2, bruToJsonV2, collectionBruToJson: _collectionBruToJson } = require('@usebruno/lang'); +const { + parseRequest: _parseRequest, + parseCollection: _parseCollection +} = require('@usebruno/filestore'); const collectionBruToJson = (bru) => { try { - const json = _collectionBruToJson(bru); + const json = _parseCollection(bru); const transformedJson = { request: { @@ -46,7 +49,7 @@ const collectionBruToJson = (bru) => { */ const bruToJson = (bru) => { try { - const json = bruToJsonV2(bru); + const json = _parseRequest(bru); let requestType = _.get(json, 'meta.type'); if (requestType === 'http') { @@ -88,14 +91,6 @@ const bruToJson = (bru) => { } }; -const bruToEnvJson = (bru) => { - try { - return bruToEnvJsonV2(bru); - } catch (err) { - return Promise.reject(err); - } -}; - const getEnvVars = (environment = {}) => { const variables = environment.variables; if (!variables || !variables.length) { @@ -119,7 +114,6 @@ const getOptions = () => { module.exports = { bruToJson, - bruToEnvJson, getEnvVars, getOptions, collectionBruToJson diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index 09d78506c..185a682ba 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -2,9 +2,8 @@ const { get, each, find, compact } = require('lodash'); const os = require('os'); const fs = require('fs'); const path = require('path'); -const { jsonToBruV2, envJsonToBruV2, jsonToCollectionBru } = require('@usebruno/lang'); const { sanitizeName } = require('./filesystem'); -const { bruToJson, collectionBruToJson } = require('./bru'); +const { parseRequest, parseCollection, parseFolder, stringifyCollection, stringifyFolder, stringifyEnvironment } = require('@usebruno/filestore'); const constants = require('../constants'); const chalk = require('chalk'); @@ -46,7 +45,7 @@ const createCollectionJsonFromPathname = (collectionPath) => { // get the request item const bruContent = fs.readFileSync(filePath, 'utf8'); - const requestItem = bruToJson(bruContent); + const requestItem = parseRequest(bruContent); currentDirItems.push({ name: file, pathname: filePath, @@ -97,7 +96,7 @@ const getCollectionRoot = (dir) => { } const content = fs.readFileSync(collectionRootPath, 'utf8'); - return collectionBruToJson(content); + return parseCollection(content); }; const getFolderRoot = (dir) => { @@ -108,7 +107,7 @@ const getFolderRoot = (dir) => { } const content = fs.readFileSync(folderRootPath, 'utf8'); - return collectionBruToJson(content); + return parseFolder(content); }; const mergeHeaders = (collection, request, requestTreePath) => { @@ -417,7 +416,7 @@ const createCollectionFromBrunoObject = async (collection, dirPath) => { // Create collection.bru if root exists if (collection.root) { - const collectionContent = await jsonToCollectionBru(collection.root); + const collectionContent = await stringifyCollection(collection.root); fs.writeFileSync(path.join(dirPath, 'collection.bru'), collectionContent); } @@ -427,7 +426,7 @@ const createCollectionFromBrunoObject = async (collection, dirPath) => { fs.mkdirSync(envDirPath, { recursive: true }); for (const env of collection.environments) { - const content = await envJsonToBruV2(env); + const content = await stringifyEnvironment(env); const filename = sanitizeName(`${env.name}.bru`); fs.writeFileSync(path.join(envDirPath, filename), content); } @@ -459,10 +458,7 @@ const processCollectionItems = async (items = [], currentPath) => { if (item.seq) { item.root.meta.seq = item.seq; } - const folderContent = await jsonToCollectionBru( - item.root, - true - ); + const folderContent = await stringifyFolder(item.root); safeWriteFileSync(folderBruFilePath, folderContent); } @@ -506,7 +502,7 @@ const processCollectionItems = async (items = [], currentPath) => { }; // Convert to BRU format and write to file - const content = await jsonToBruV2(bruJson); + const content = await stringifyRequest(bruJson); safeWriteFileSync(path.join(currentPath, sanitizedFilename), content); } } diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index d7303e2fb..b398cc9bf 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -38,6 +38,7 @@ "@usebruno/schema": "0.7.0", "@usebruno/vm2": "^3.9.13", "@usebruno/requests": "^0.1.0", + "@usebruno/filestore": "^0.1.0", "about-window": "^1.15.2", "aws4-axios": "^3.3.0", "axios": "^1.8.3", diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js index d6b6cbfba..908e04ccf 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -3,8 +3,14 @@ const fs = require('fs'); const path = require('path'); const chokidar = require('chokidar'); const { hasBruExtension, isWSLPath, normalizeAndResolvePath, sizeInMB } = require('../utils/filesystem'); -const { bruToEnvJson, bruToJson, bruToJsonViaWorker, collectionBruToJson } = require('../bru'); -const { dotenvToJson } = require('@usebruno/lang'); +const { + parseEnvironment, + parseRequest, + parseRequestViaWorker, + parseCollection, + parseFolder +} = require('@usebruno/filestore'); +const { parseDotEnv } = require('@usebruno/filestore'); const { uuid } = require('../utils/common'); const { getRequestUid } = require('../cache/requestUids'); @@ -80,7 +86,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath) let bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = await bruToEnvJson(bruContent); + file.data = await parseEnvironment(bruContent); file.data.name = basename.substring(0, basename.length - 4); file.data.uid = getRequestUid(pathname); @@ -115,7 +121,7 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat }; const bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = await bruToEnvJson(bruContent); + file.data = await parseEnvironment(bruContent); file.data.name = basename.substring(0, basename.length - 4); file.data.uid = getRequestUid(pathname); _.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid())); @@ -177,7 +183,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread if (isDotEnvFile(pathname, collectionPath)) { try { const content = fs.readFileSync(pathname, 'utf8'); - const jsonData = dotenvToJson(content); + const jsonData = parseDotEnv(content); setDotEnvVars(collectionUid, jsonData); const payload = { @@ -209,7 +215,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread try { let bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = await collectionBruToJson(bruContent); + file.data = await parseCollection(bruContent); hydrateBruCollectionFileWithUuid(file.data); win.webContents.send('main:collection-tree-updated', 'addFile', file); @@ -233,7 +239,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread try { let bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = await collectionBruToJson(bruContent); + file.data = await parseCollection(bruContent); hydrateBruCollectionFileWithUuid(file.data); win.webContents.send('main:collection-tree-updated', 'addFile', file); @@ -258,7 +264,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread // If worker thread is not used, we can directly parse the file if (!useWorkerThread) { try { - file.data = await bruToJson(bruContent); + file.data = await parseRequest(bruContent); file.partial = false; file.loading = false; file.size = sizeInMB(fileStats?.size); @@ -278,7 +284,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread type: 'http-request' }; - const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + const metaJson = parseBruFileMeta(bruContent); file.data = metaJson; file.partial = true; file.loading = false; @@ -295,7 +301,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread win.webContents.send('main:collection-tree-updated', 'addFile', file); // This is to update the file info in the UI - file.data = await bruToJsonViaWorker(bruContent); + file.data = await parseRequestViaWorker(bruContent); file.partial = false; file.loading = false; hydrateRequestWithUuid(file.data, pathname); @@ -331,7 +337,7 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => { if (fs.existsSync(folderBruFilePath)) { let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8'); - let folderBruData = await collectionBruToJson(folderBruFileContent); + let folderBruData = await parseFolder(folderBruFileContent); name = folderBruData?.meta?.name || name; seq = folderBruData?.meta?.seq; } @@ -370,7 +376,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => { if (isDotEnvFile(pathname, collectionPath)) { try { const content = fs.readFileSync(pathname, 'utf8'); - const jsonData = dotenvToJson(content); + const jsonData = parseDotEnv(content); setDotEnvVars(collectionUid, jsonData); const payload = { @@ -402,7 +408,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => { try { let bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = await collectionBruToJson(bruContent); + file.data = await parseCollection(bruContent); hydrateBruCollectionFileWithUuid(file.data); win.webContents.send('main:collection-tree-updated', 'change', file); return; @@ -425,7 +431,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => { try { let bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = await collectionBruToJson(bruContent); + file.data = await parseCollection(bruContent); hydrateBruCollectionFileWithUuid(file.data); win.webContents.send('main:collection-tree-updated', 'change', file); @@ -447,7 +453,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => { }; const bru = fs.readFileSync(pathname, 'utf8'); - file.data = await bruToJson(bru); + file.data = await parseRequest(bru); hydrateRequestWithUuid(file.data, pathname); win.webContents.send('main:collection-tree-updated', 'change', file); @@ -490,7 +496,7 @@ const unlinkDir = async (win, pathname, collectionUid, collectionPath) => { if (fs.existsSync(folderBruFilePath)) { let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8'); - let folderBruData = await collectionBruToJson(folderBruFileContent); + let folderBruData = await parseFolder(folderBruFileContent); name = folderBruData?.meta?.name || name; } diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js deleted file mode 100644 index 9dd920d8d..000000000 --- a/packages/bruno-electron/src/bru/index.js +++ /dev/null @@ -1,279 +0,0 @@ -const _ = require('lodash'); -const { - bruToJsonV2, - jsonToBruV2, - bruToEnvJsonV2, - envJsonToBruV2, - collectionBruToJson: _collectionBruToJson, - jsonToCollectionBru: _jsonToCollectionBru -} = require('@usebruno/lang'); -const BruParserWorker = require('./workers'); - -const bruParserWorker = new BruParserWorker(); - -const collectionBruToJson = async (data, parsed = false) => { - try { - const json = parsed ? data : _collectionBruToJson(data); - - const transformedJson = { - request: { - headers: _.get(json, 'headers', []), - auth: _.get(json, 'auth', {}), - script: _.get(json, 'script', {}), - vars: _.get(json, 'vars', {}), - tests: _.get(json, 'tests', '') - }, - settings: _.get(json, 'settings', {}), - docs: _.get(json, 'docs', '') - }; - - // add meta if it exists - // this is only for folder bru file - // in the future, all of this will be replaced by standard bru lang - const sequence = _.get(json, 'meta.seq'); - if (json?.meta) { - transformedJson.meta = { - name: json.meta.name, - }; - - if (sequence) { - transformedJson.meta.seq = Number(sequence); - } - } - - return transformedJson; - } catch (error) { - return Promise.reject(error); - } -}; - -const jsonToCollectionBru = async (json, isFolder) => { - try { - const collectionBruJson = { - headers: _.get(json, 'request.headers', []), - script: { - req: _.get(json, 'request.script.req', ''), - res: _.get(json, 'request.script.res', '') - }, - vars: { - req: _.get(json, 'request.vars.req', []), - res: _.get(json, 'request.vars.res', []) - }, - tests: _.get(json, 'request.tests', ''), - auth: _.get(json, 'request.auth', {}), - docs: _.get(json, 'docs', '') - }; - - // add meta if it exists - // this is only for folder bru file - // in the future, all of this will be replaced by standard bru lang - const sequence = _.get(json, 'meta.seq'); - if (json?.meta) { - collectionBruJson.meta = { - name: json.meta.name, - }; - - if (sequence) { - collectionBruJson.meta.seq = Number(sequence); - } - } - - return _jsonToCollectionBru(collectionBruJson); - } catch (error) { - return Promise.reject(error); - } -}; - -const bruToEnvJson = async (bru) => { - try { - const json = bruToEnvJsonV2(bru); - - // the app env format requires each variable to have a type - // this need to be evaluated and safely removed - // i don't see it being used in schema validation - if (json && json.variables && json.variables.length) { - _.each(json.variables, (v) => (v.type = 'text')); - } - - return json; - } catch (error) { - return Promise.reject(error); - } -}; - -const envJsonToBru = async (json) => { - try { - const bru = envJsonToBruV2(json); - return bru; - } catch (error) { - return Promise.reject(error); - } -}; - -/** - * The transformer function for converting a BRU file to JSON. - * - * We map the json response from the bru lang and transform it into the DSL - * format that the app uses - * - * @param {string} data The BRU file content. - * @returns {object} The JSON representation of the BRU file. - */ -const bruToJson = (data, parsed = false) => { - try { - const json = parsed ? data : bruToJsonV2(data); - - let requestType = _.get(json, 'meta.type'); - if (requestType === 'http') { - requestType = 'http-request'; - } else if (requestType === 'graphql') { - requestType = 'graphql-request'; - } else { - requestType = 'http-request'; - } - - const sequence = _.get(json, 'meta.seq'); - const transformedJson = { - type: requestType, - name: _.get(json, 'meta.name'), - seq: !_.isNaN(sequence) ? Number(sequence) : 1, - settings: _.get(json, 'settings', {}), - tags: _.get(json, 'meta.tags', []), - request: { - method: _.upperCase(_.get(json, 'http.method')), - url: _.get(json, 'http.url'), - params: _.get(json, 'params', []), - headers: _.get(json, 'headers', []), - auth: _.get(json, 'auth', {}), - body: _.get(json, 'body', {}), - script: _.get(json, 'script', {}), - vars: _.get(json, 'vars', {}), - assertions: _.get(json, 'assertions', []), - tests: _.get(json, 'tests', ''), - docs: _.get(json, 'docs', '') - } - }; - - transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none'); - transformedJson.request.body.mode = _.get(json, 'http.body', 'none'); - return transformedJson; - } catch (e) { - return Promise.reject(e); - } -}; - -const bruToJsonViaWorker = async (data) => { - try { - const json = await bruParserWorker?.bruToJson(data); - return bruToJson(json, true); - } catch (e) { - return Promise.reject(e); - } -}; - -/** - * The transformer function for converting a JSON to BRU file. - * - * We map the json response from the app and transform it into the DSL - * format that the bru lang understands - * - * @param {object} json The JSON representation of the BRU file. - * @returns {string} The BRU file content. - */ -const jsonToBru = async (json) => { - let type = _.get(json, 'type'); - if (type === 'http-request') { - type = 'http'; - } else if (type === 'graphql-request') { - type = 'graphql'; - } else { - type = 'http'; - } - - const sequence = _.get(json, 'seq'); - const bruJson = { - meta: { - name: _.get(json, 'name'), - type: type, - seq: !_.isNaN(sequence) ? Number(sequence) : 1, - tags: _.get(json, 'tags', []), - }, - http: { - method: _.lowerCase(_.get(json, 'request.method')), - url: _.get(json, 'request.url'), - auth: _.get(json, 'request.auth.mode', 'none'), - body: _.get(json, 'request.body.mode', 'none') - }, - params: _.get(json, 'request.params', []), - headers: _.get(json, 'request.headers', []), - auth: _.get(json, 'request.auth', {}), - body: _.get(json, 'request.body', {}), - script: _.get(json, 'request.script', {}), - vars: { - req: _.get(json, 'request.vars.req', []), - res: _.get(json, 'request.vars.res', []) - }, - assertions: _.get(json, 'request.assertions', []), - tests: _.get(json, 'request.tests', ''), - settings: _.get(json, 'settings', {}), - docs: _.get(json, 'request.docs', '') - }; - - const bru = jsonToBruV2(bruJson); - return bru; -}; - -const jsonToBruViaWorker = async (json) => { - let type = _.get(json, 'type'); - if (type === 'http-request') { - type = 'http'; - } else if (type === 'graphql-request') { - type = 'graphql'; - } else { - type = 'http'; - } - - const sequence = _.get(json, 'seq'); - const bruJson = { - meta: { - name: _.get(json, 'name'), - type: type, - seq: !_.isNaN(sequence) ? Number(sequence) : 1, - tags: _.get(json, 'tags', []) - }, - http: { - method: _.lowerCase(_.get(json, 'request.method')), - url: _.get(json, 'request.url'), - auth: _.get(json, 'request.auth.mode', 'none'), - body: _.get(json, 'request.body.mode', 'none') - }, - params: _.get(json, 'request.params', []), - headers: _.get(json, 'request.headers', []), - auth: _.get(json, 'request.auth', {}), - body: _.get(json, 'request.body', {}), - script: _.get(json, 'request.script', {}), - vars: { - req: _.get(json, 'request.vars.req', []), - res: _.get(json, 'request.vars.res', []) - }, - assertions: _.get(json, 'request.assertions', []), - tests: _.get(json, 'request.tests', ''), - settings: _.get(json, 'settings', {}), - docs: _.get(json, 'request.docs', '') - }; - - const bru = await bruParserWorker?.jsonToBru(bruJson) - return bru; -}; - - -module.exports = { - bruToJson, - bruToJsonViaWorker, - jsonToBru, - bruToEnvJson, - envJsonToBru, - collectionBruToJson, - jsonToCollectionBru, - jsonToBruViaWorker -}; diff --git a/packages/bruno-electron/src/bru/workers/index.js b/packages/bruno-electron/src/bru/workers/index.js deleted file mode 100644 index 51030b9ed..000000000 --- a/packages/bruno-electron/src/bru/workers/index.js +++ /dev/null @@ -1,64 +0,0 @@ -const { sizeInMB } = require("../../utils/filesystem"); -const WorkerQueue = require("../../workers"); -const path = require("path"); - -const getSize = (data) => { - return sizeInMB(typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8')); -} - -/** - * Lanes are used to determine which worker queue to use based on the size of the data. - * - * The first lane is for smaller files (<0.1MB), the second lane is for larger files (>=0.1MB). - * This helps with parsing performance. - */ -const LANES = [{ - maxSize: 0.005 -},{ - maxSize: 0.1 -},{ - maxSize: 1 -},{ - maxSize: 10 -},{ - maxSize: 100 -}]; - -class BruParserWorker { - constructor() { - this.workerQueues = LANES?.map(lane => ({ - maxSize: lane?.maxSize, - workerQueue: new WorkerQueue() - })); - } - - getWorkerQueue(size) { - // Find the first queue that can handle the given size - // or fallback to the last queue for largest files - const queueForSize = this.workerQueues.find((queue) => - queue.maxSize >= size - ); - - return queueForSize?.workerQueue ?? this.workerQueues.at(-1).workerQueue; - } - - async enqueueTask({data, scriptFile }) { - const size = getSize(data); - const workerQueue = this.getWorkerQueue(size); - return workerQueue.enqueue({ - data, - priority: size, - scriptPath: path.join(__dirname, `./scripts/${scriptFile}.js`) - }); - } - - async bruToJson(data) { - return this.enqueueTask({ data, scriptFile: `bru-to-json` }); - } - - async jsonToBru(data) { - return this.enqueueTask({ data, scriptFile: `json-to-bru` }); - } -} - -module.exports = BruParserWorker; \ No newline at end of file diff --git a/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js b/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js deleted file mode 100644 index 92086c4b6..000000000 --- a/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js +++ /dev/null @@ -1,16 +0,0 @@ -const { parentPort } = require('worker_threads'); -const { - bruToJsonV2, -} = require('@usebruno/lang'); - -parentPort.on('message', (workerData) => { - try { - const bru = workerData; - const json = bruToJsonV2(bru); - parentPort.postMessage(json); - } - catch(error) { - console.error(error); - parentPort.postMessage({ error: error?.message }); - } -}); \ No newline at end of file diff --git a/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js b/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js deleted file mode 100644 index c2a4f88e4..000000000 --- a/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js +++ /dev/null @@ -1,16 +0,0 @@ -const { parentPort } = require('worker_threads'); -const { - jsonToBruV2, -} = require('@usebruno/lang'); - -parentPort.on('message', (workerData) => { - try { - const json = workerData; - const bru = jsonToBruV2(json); - parentPort.postMessage(bru); - } - catch(error) { - console.error(error); - parentPort.postMessage({ error: error?.message }); - } -}); \ No newline at end of file diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 188f22b6d..089507334 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -5,7 +5,18 @@ const fsExtra = require('fs-extra'); const os = require('os'); const path = require('path'); const { ipcMain, shell, dialog, app } = require('electron'); -const { envJsonToBru, bruToJson, jsonToBru, jsonToBruViaWorker, collectionBruToJson, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru'); +const { + parseRequest, + stringifyRequest, + parseRequestViaWorker, + stringifyRequestViaWorker, + parseCollection, + stringifyCollection, + parseFolder, + stringifyFolder, + parseEnvironment, + stringifyEnvironment +} = require('@usebruno/filestore'); const brunoConverters = require('@usebruno/converters'); const { postmanToBruno } = brunoConverters; @@ -225,10 +236,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }; } - const content = await jsonToCollectionBru( - folderRoot, - true // isFolder - ); + const content = await stringifyFolder(folderRoot); await writeFile(folderBruFilePath, content); } catch (error) { return Promise.reject(error); @@ -238,7 +246,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection try { const collectionBruFilePath = path.join(collectionPathname, 'collection.bru'); - const content = await jsonToCollectionBru(collectionRoot); + const content = await stringifyCollection(collectionRoot); await writeFile(collectionBruFilePath, content); } catch (error) { return Promise.reject(error); @@ -256,7 +264,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(`${request.filename}.bru is not a valid filename`); } validatePathIsInsideCollection(pathname, lastOpenedCollections); - const content = await jsonToBruViaWorker(request); + const content = await stringifyRequestViaWorker(request); await writeFile(pathname, content); } catch (error) { return Promise.reject(error); @@ -270,7 +278,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(`path: ${pathname} does not exist`); } - const content = await jsonToBruViaWorker(request); + const content = await stringifyRequestViaWorker(request); await writeFile(pathname, content); } catch (error) { return Promise.reject(error); @@ -288,7 +296,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(`path: ${pathname} does not exist`); } - const content = await jsonToBruViaWorker(request); + const content = await stringifyRequestViaWorker(request); await writeFile(pathname, content); } } catch (error) { @@ -318,7 +326,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection environmentSecretsStore.storeEnvSecrets(collectionPathname, environment); } - const content = await envJsonToBru(environment); + const content = await stringifyEnvironment(environment); await writeFile(envFilePath, content); } catch (error) { @@ -343,7 +351,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection environmentSecretsStore.storeEnvSecrets(collectionPathname, environment); } - const content = await envJsonToBru(environment); + const content = await stringifyEnvironment(environment); await writeFile(envFilePath, content); } catch (error) { return Promise.reject(error); @@ -402,7 +410,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection let folderBruFileJsonContent; if (fs.existsSync(folderBruFilePath)) { const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); - folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); + folderBruFileJsonContent = await parseFolder(oldFolderBruFileContent); folderBruFileJsonContent.meta.name = newName; } else { folderBruFileJsonContent = { @@ -412,7 +420,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }; } - const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); + const folderBruFileContent = await stringifyFolder(folderBruFileJsonContent); await writeFile(folderBruFilePath, folderBruFileContent); return; @@ -424,9 +432,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } const data = fs.readFileSync(itemPath, 'utf8'); - const jsonData = await bruToJson(data); + const jsonData = parseRequest(data); jsonData.name = newName; - const content = await jsonToBru(jsonData); + const content = stringifyRequest(jsonData); await writeFile(itemPath, content); } catch (error) { return Promise.reject(error); @@ -452,7 +460,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection let folderBruFileJsonContent; if (fs.existsSync(folderBruFilePath)) { const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); - folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); + folderBruFileJsonContent = await parseFolder(oldFolderBruFileContent); folderBruFileJsonContent.meta.name = newName; } else { folderBruFileJsonContent = { @@ -462,7 +470,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }; } - const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); + const folderBruFileContent = await stringifyFolder(folderBruFileJsonContent); await writeFile(folderBruFilePath, folderBruFileContent); const bruFilesAtSource = await searchForBruFiles(oldPath); @@ -503,11 +511,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // update name in file and save new copy, then delete old copy const data = await fs.promises.readFile(oldPath, 'utf8'); // Use async read - const jsonData = await bruToJsonViaWorker(data); + const jsonData = parseRequest(data); jsonData.name = newName; moveRequestUid(oldPath, newPath); - const content = await jsonToBruViaWorker(jsonData); + const content = stringifyRequest(jsonData); await fs.promises.unlink(oldPath); await writeFile(newPath, content); @@ -538,7 +546,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (!fs.existsSync(pathname)) { fs.mkdirSync(pathname); const folderBruFilePath = path.join(pathname, 'folder.bru'); - const content = await jsonToCollectionBru(folderBruJsonData, true); // isFolder flag + const content = await stringifyFolder(folderBruJsonData); await writeFile(folderBruFilePath, content); } else { return Promise.reject(new Error('The directory already exists')); @@ -611,7 +619,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection items.forEach(async (item) => { if (['http-request', 'graphql-request'].includes(item.type)) { let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`); - const content = await jsonToBruViaWorker(item); + const content = await stringifyRequestViaWorker(item); const filePath = path.join(currentPath, sanitizedFilename); safeWriteFileSync(filePath, content); } @@ -623,10 +631,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (item?.root?.meta?.name) { const folderBruFilePath = path.join(folderPath, 'folder.bru'); item.root.meta.seq = item.seq; - const folderContent = await jsonToCollectionBru( - item.root, - true // isFolder - ); + const folderContent = await stringifyFolder(item.root); safeWriteFileSync(folderBruFilePath, folderContent); } @@ -650,7 +655,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } environments.forEach(async (env) => { - const content = await envJsonToBru(env); + const content = await stringifyEnvironment(env); let sanitizedEnvFilename = sanitizeName(`${env.name}.bru`); const filePath = path.join(envDirPath, sanitizedEnvFilename); safeWriteFileSync(filePath, content); @@ -681,7 +686,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // Write the Bruno configuration to a file await writeFile(path.join(collectionPath, 'bruno.json'), stringifiedBrunoConfig); - const collectionContent = await jsonToCollectionBru(collection.root); + const collectionContent = await stringifyCollection(collection.root); await writeFile(path.join(collectionPath, 'collection.bru'), collectionContent); const { size, filesCount } = await getCollectionStats(collectionPath); @@ -711,7 +716,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const parseCollectionItems = (items = [], currentPath) => { items.forEach(async (item) => { if (['http-request', 'graphql-request'].includes(item.type)) { - const content = await jsonToBruViaWorker(item); + const content = await stringifyRequestViaWorker(item); const filePath = path.join(currentPath, item.filename); safeWriteFileSync(filePath, content); } @@ -721,7 +726,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // If folder has a root element, then I should write its folder.bru file if (item.root) { - const folderContent = await jsonToCollectionBru(item.root, true); + const folderContent = await stringifyFolder(item.root); folderContent.name = item.name; if (folderContent) { const bruFolderPath = path.join(folderPath, `folder.bru`); @@ -740,7 +745,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // If initial folder has a root element, then I should write its folder.bru file if (itemFolder.root) { - const folderContent = await jsonToCollectionBru(itemFolder.root, true); + const folderContent = await stringifyFolder(itemFolder.root); if (folderContent) { const bruFolderPath = path.join(collectionPath, `folder.bru`); safeWriteFileSync(bruFolderPath, folderContent); @@ -767,7 +772,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }; if (fs.existsSync(folderRootPath)) { const bru = fs.readFileSync(folderRootPath, 'utf8'); - folderBruJsonData = await collectionBruToJson(bru); + folderBruJsonData = await parseCollection(bru); if (!folderBruJsonData?.meta) { folderBruJsonData.meta = { name: path.basename(item.pathname), @@ -779,12 +784,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } folderBruJsonData.meta.seq = item.seq; } - const content = await jsonToCollectionBru(folderBruJsonData); + const content = await stringifyFolder(folderBruJsonData); await writeFile(folderRootPath, content); } else { if (fs.existsSync(item.pathname)) { const itemToSave = transformRequestToSaveToFilesystem(item); - const content = await jsonToBruViaWorker(itemToSave); + const content = await stringifyRequestViaWorker(itemToSave); await writeFile(item.pathname, content); } } @@ -1065,14 +1070,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }; let bruContent = fs.readFileSync(pathname, 'utf8'); - const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + const metaJson = parseBruFileMeta(bruContent); file.data = metaJson; file.loading = true; file.partial = true; file.size = sizeInMB(fileStats?.size); hydrateRequestWithUuid(file.data, pathname); mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); - file.data = await bruToJsonViaWorker(bruContent); + file.data = await parseRequestViaWorker(bruContent); file.partial = false; file.loading = true; file.size = sizeInMB(fileStats?.size); @@ -1089,7 +1094,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }; let bruContent = fs.readFileSync(pathname, 'utf8'); - const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + const metaJson = parseRequest(parseBruFileMeta(bruContent)); file.data = metaJson; file.partial = true; file.loading = false; @@ -1140,14 +1145,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }; let bruContent = fs.readFileSync(pathname, 'utf8'); - const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + const metaJson = parseRequest(parseBruFileMeta(bruContent)); file.data = metaJson; file.loading = true; file.partial = true; file.size = sizeInMB(fileStats?.size); hydrateRequestWithUuid(file.data, pathname); mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); - file.data = bruToJson(bruContent); + file.data = parseRequest(bruContent); file.partial = false; file.loading = true; file.size = sizeInMB(fileStats?.size); @@ -1164,7 +1169,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }; let bruContent = fs.readFileSync(pathname, 'utf8'); - const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + const metaJson = parseRequest(parseBruFileMeta(bruContent)); file.data = metaJson; file.partial = true; file.loading = false; diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index c7120779e..b502fe4a5 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -237,12 +237,47 @@ const parseBruFileMeta = (data) => { metaJson[key] = isNaN(value) ? value : Number(value); } }); - return { meta: metaJson }; + + // Transform to the format expected by bruno-app + let requestType = metaJson.type; + if (requestType === 'http') { + requestType = 'http-request'; + } else if (requestType === 'graphql') { + requestType = 'graphql-request'; + } else { + requestType = 'http-request'; + } + + const sequence = metaJson.seq; + const transformedJson = { + type: requestType, + name: metaJson.name, + seq: !isNaN(sequence) ? Number(sequence) : 1, + settings: {}, + tags: metaJson.tags || [], + request: { + method: '', + url: '', + params: [], + headers: [], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: {}, + vars: {}, + assertions: [], + tests: '', + docs: '' + } + }; + + return transformedJson; } else { console.log('No "meta" block found in the file.'); + return null; } } catch (err) { console.error('Error reading file:', err); + return null; } } diff --git a/packages/bruno-electron/src/workers/index.js b/packages/bruno-electron/src/workers/index.js deleted file mode 100644 index d1d1a1b74..000000000 --- a/packages/bruno-electron/src/workers/index.js +++ /dev/null @@ -1,68 +0,0 @@ -const { Worker } = require('worker_threads'); - -class WorkerQueue { - constructor() { - this.queue = []; - this.isProcessing = false; - this.workers = {}; - } - - async getWorkerForScriptPath(scriptPath) { - if (!this.workers) this.workers = {}; - let worker = this.workers[scriptPath]; - if (!worker || worker.threadId === -1) { - this.workers[scriptPath] = worker = new Worker(scriptPath); - } - return worker; - } - - async enqueue(task) { - const { priority, scriptPath, data } = task; - - return new Promise((resolve, reject) => { - this.queue.push({ priority, scriptPath, data, resolve, reject }); - this.queue?.sort((taskX, taskY) => taskX?.priority - taskY?.priority); - this.processQueue(); - }); - } - - async processQueue() { - if (this.isProcessing || this.queue.length === 0){ - return; - } - - this.isProcessing = true; - const { scriptPath, data, resolve, reject } = this.queue.shift(); - - try { - const result = await this.runWorker({ scriptPath, data }); - resolve(result); - } catch (error) { - reject(error); - } finally { - this.isProcessing = false; - this.processQueue(); - } - } - - async runWorker({ scriptPath, data }) { - return new Promise(async (resolve, reject) => { - let worker = await this.getWorkerForScriptPath(scriptPath); - worker.postMessage(data); - worker.on('message', (data) => { - if (data?.error) { - reject(new Error(data?.error)); - } - resolve(data); - }); - worker.on('error', (error) => { - reject(error); - }); - worker.on('exit', (code) => { - reject(new Error(`stopped with ${code} exit code`)); - }); - }); - } -} - -module.exports = WorkerQueue; diff --git a/packages/bruno-electron/tests/utils/collection.spec.js b/packages/bruno-electron/tests/utils/collection.spec.js index 4efc9c002..363537db6 100644 --- a/packages/bruno-electron/tests/utils/collection.spec.js +++ b/packages/bruno-electron/tests/utils/collection.spec.js @@ -11,22 +11,35 @@ describe('parseBruFileMeta', () => { const result = parseBruFileMeta(data); expect(result).toEqual({ - meta: { - name: '0.2_mb', - type: 'http', - seq: 1, - }, + type: 'http-request', + name: '0.2_mb', + seq: 1, + settings: {}, + tags: [], + request: { + method: '', + url: '', + params: [], + headers: [], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: {}, + vars: {}, + assertions: [], + tests: '', + docs: '' + } }); }); - test('returns undefined for missing meta block', () => { + test('returns null for missing meta block', () => { const data = `someOtherBlock { key: value }`; const result = parseBruFileMeta(data); - expect(result).toBeUndefined(); + expect(result).toBeNull(); }); test('handles empty meta block gracefully', () => { @@ -34,7 +47,26 @@ describe('parseBruFileMeta', () => { const result = parseBruFileMeta(data); - expect(result).toEqual({ meta: {} }); + expect(result).toEqual({ + type: 'http-request', + name: undefined, + seq: 1, + settings: {}, + tags: [], + request: { + method: '', + url: '', + params: [], + headers: [], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: {}, + vars: {}, + assertions: [], + tests: '', + docs: '' + } + }); }); test('ignores invalid lines in meta block', () => { @@ -47,10 +79,24 @@ describe('parseBruFileMeta', () => { const result = parseBruFileMeta(data); expect(result).toEqual({ - meta: { - name: '0.2_mb', - seq: 1, - }, + type: 'http-request', + name: '0.2_mb', + seq: 1, + settings: {}, + tags: [], + request: { + method: '', + url: '', + params: [], + headers: [], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: {}, + vars: {}, + assertions: [], + tests: '', + docs: '' + } }); }); @@ -59,7 +105,7 @@ describe('parseBruFileMeta', () => { const result = parseBruFileMeta(data); - expect(result).toBeUndefined(); + expect(result).toBeNull(); }); test('handles missing colon gracefully', () => { @@ -71,9 +117,24 @@ describe('parseBruFileMeta', () => { const result = parseBruFileMeta(data); expect(result).toEqual({ - meta: { - seq: 1, - }, + type: 'http-request', + name: undefined, + seq: 1, + settings: {}, + tags: [], + request: { + method: '', + url: '', + params: [], + headers: [], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: {}, + vars: {}, + assertions: [], + tests: '', + docs: '' + } }); }); @@ -82,16 +143,30 @@ describe('parseBruFileMeta', () => { numValue: 1234 floatValue: 12.34 strValue: some_text + seq: 5 }`; const result = parseBruFileMeta(data); expect(result).toEqual({ - meta: { - numValue: 1234, - floatValue: 12.34, - strValue: 'some_text', - }, + type: 'http-request', + name: undefined, + seq: 5, + settings: {}, + tags: [], + request: { + method: '', + url: '', + params: [], + headers: [], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: {}, + vars: {}, + assertions: [], + tests: '', + docs: '' + } }); }); @@ -104,7 +179,7 @@ describe('parseBruFileMeta', () => { const result = parseBruFileMeta(data); - expect(result).toBeUndefined(); + expect(result).toBeNull(); }); test('handles syntax error in meta block 2', () => { @@ -116,6 +191,98 @@ describe('parseBruFileMeta', () => { const result = parseBruFileMeta(data); - expect(result).toBeUndefined(); + expect(result).toBeNull(); }); -}); + + test('handles graphql type correctly', () => { + const data = `meta { + name: graphql_query + type: graphql + seq: 2 + }`; + + const result = parseBruFileMeta(data); + + expect(result).toEqual({ + type: 'graphql-request', + name: 'graphql_query', + seq: 2, + settings: {}, + tags: [], + request: { + method: '', + url: '', + params: [], + headers: [], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: {}, + vars: {}, + assertions: [], + tests: '', + docs: '' + } + }); + }); + + test('handles unknown type correctly', () => { + const data = `meta { + name: unknown_request + type: unknown + seq: 3 + }`; + + const result = parseBruFileMeta(data); + + expect(result).toEqual({ + type: 'http-request', + name: 'unknown_request', + seq: 3, + settings: {}, + tags: [], + request: { + method: '', + url: '', + params: [], + headers: [], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: {}, + vars: {}, + assertions: [], + tests: '', + docs: '' + } + }); + }); + + test('handles missing seq gracefully', () => { + const data = `meta { + name: no_seq_request + type: http + }`; + + const result = parseBruFileMeta(data); + + expect(result).toEqual({ + type: 'http-request', + name: 'no_seq_request', + seq: 1, // Default fallback + settings: {}, + tags: [], + request: { + method: '', + url: '', + params: [], + headers: [], + auth: { mode: 'none' }, + body: { mode: 'none' }, + script: {}, + vars: {}, + assertions: [], + tests: '', + docs: '' + } + }); + }); +}); \ No newline at end of file diff --git a/packages/bruno-filestore/.gitignore b/packages/bruno-filestore/.gitignore new file mode 100644 index 000000000..a05291c3a --- /dev/null +++ b/packages/bruno-filestore/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +*.log +dist +coverage \ No newline at end of file diff --git a/packages/bruno-filestore/LICENSE.md b/packages/bruno-filestore/LICENSE.md new file mode 100644 index 000000000..f88e206bb --- /dev/null +++ b/packages/bruno-filestore/LICENSE.md @@ -0,0 +1,22 @@ + +MIT License + +Copyright (c) 2022 Anoop M D, Anusree P S and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/bruno-filestore/README.md b/packages/bruno-filestore/README.md new file mode 100644 index 000000000..8abc92868 --- /dev/null +++ b/packages/bruno-filestore/README.md @@ -0,0 +1,50 @@ +# Bruno Filestore + +A generic file storage and parsing package for Bruno API client. + +## Purpose + +This package abstracts the file format operations for Bruno, providing a clean interface for parsing and stringifying Bruno requests, collections, folders, and environments. + +## Features + +- Format-agnostic APIs for file operations +- Currently supports Bruno's custom `.bru` format +- Designed for future extensibility to support YAML and other formats + +## Usage + +```javascript +const { + parseRequest, + stringifyRequest, + parseCollection, + stringifyCollection, + parseEnvironment, + stringifyEnvironment, + parseDotEnv +} = require('@usebruno/filestore'); + +// Parse a .bru request file +const requestData = parseRequest(bruContent); + +// Stringify request data to .bru format +const bruContent = stringifyRequest(requestData); + +// Example with future format support (not yet implemented) +const requestData = parseRequest(yamlContent, { format: 'yaml' }); +``` + +## API + +The package provides the following functions: + +- `parseRequest(content, options = { format: 'bru' })`: Parse request file content +- `stringifyRequest(requestObj, options = { format: 'bru' })`: Convert request object to file content +- `parseCollection(content, options = { format: 'bru' })`: Parse collection file content +- `stringifyCollection(collectionObj, options = { format: 'bru' })`: Convert collection object to file content +- `parseFolder(content, options = { format: 'bru' })`: Parse folder file content +- `stringifyFolder(folderObj, options = { format: 'bru' })`: Convert folder object to file content +- `parseEnvironment(content, options = { format: 'bru' })`: Parse environment file content +- `stringifyEnvironment(envObj, options = { format: 'bru' })`: Convert environment object to file content +- `parseDotEnv(content)`: Parse .env file content \ No newline at end of file diff --git a/packages/bruno-filestore/babel.config.js b/packages/bruno-filestore/babel.config.js new file mode 100644 index 000000000..a0b85248b --- /dev/null +++ b/packages/bruno-filestore/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-typescript', + ], +}; \ No newline at end of file diff --git a/packages/bruno-filestore/jest.config.js b/packages/bruno-filestore/jest.config.js new file mode 100644 index 000000000..bf7878c22 --- /dev/null +++ b/packages/bruno-filestore/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + transform: { + '^.+\\.(js|ts)$': 'babel-jest', + }, + moduleFileExtensions: ['js', 'ts'], + testMatch: ['**/__tests__/**/*.(js|ts)', '**/*.(test|spec).(js|ts)'], + collectCoverageFrom: [ + 'src/**/*.(js|ts)', + '!src/**/*.d.ts', + ], + setupFilesAfterEnv: [], +}; \ No newline at end of file diff --git a/packages/bruno-filestore/package.json b/packages/bruno-filestore/package.json new file mode 100644 index 000000000..69558339b --- /dev/null +++ b/packages/bruno-filestore/package.json @@ -0,0 +1,46 @@ +{ + "name": "@usebruno/filestore", + "version": "0.1.0", + "license": "MIT", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "src", + "package.json" + ], + "scripts": { + "clean": "rimraf dist", + "prebuild": "npm run clean", + "build": "rollup -c", + "watch": "rollup -c -w", + "test": "jest", + "test:watch": "jest --watch", + "prepack": "npm run test && npm run build" + }, + "devDependencies": { + "@babel/preset-env": "^7.22.0", + "@babel/preset-typescript": "^7.22.0", + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-typescript": "^9.0.2", + "@types/jest": "^29.5.11", + "@types/lodash": "^4.14.191", + "babel-jest": "^29.7.0", + "jest": "^29.2.0", + "rimraf": "^3.0.2", + "rollup": "3.29.5", + "rollup-plugin-dts": "^5.0.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "rollup-plugin-terser": "^7.0.2", + "typescript": "^4.8.4" + }, + "overrides": { + "rollup": "3.29.5" + }, + "dependencies": { + "@usebruno/lang": "0.12.0", + "lodash": "^4.17.21" + } +} \ No newline at end of file diff --git a/packages/bruno-filestore/rollup.config.js b/packages/bruno-filestore/rollup.config.js new file mode 100644 index 000000000..e272dc015 --- /dev/null +++ b/packages/bruno-filestore/rollup.config.js @@ -0,0 +1,63 @@ +const { nodeResolve } = require('@rollup/plugin-node-resolve'); +const commonjs = require('@rollup/plugin-commonjs'); +const typescript = require('@rollup/plugin-typescript'); +const dts = require('rollup-plugin-dts'); +const { terser } = require('rollup-plugin-terser'); +const peerDepsExternal = require('rollup-plugin-peer-deps-external'); + +const packageJson = require('./package.json'); + +module.exports = [ + { + input: 'src/index.ts', + output: [ + { + file: packageJson.main, + format: 'cjs', + sourcemap: true, + exports: 'named' + }, + { + file: packageJson.module, + format: 'esm', + sourcemap: true, + exports: 'named' + } + ], + plugins: [ + peerDepsExternal(), + nodeResolve({ + extensions: ['.js', '.ts', '.tsx', '.json', '.css'] + }), + commonjs(), + typescript({ tsconfig: './tsconfig.json' }), + terser(), + ], + external: ['@usebruno/lang', 'lodash', 'worker_threads', 'path'] + }, + { + input: 'src/workers/worker-script.ts', + output: [ + { + file: 'dist/cjs/workers/worker-script.js', + format: 'cjs', + sourcemap: true + }, + { + file: 'dist/esm/workers/worker-script.js', + format: 'cjs', + sourcemap: true + } + ], + plugins: [ + peerDepsExternal(), + nodeResolve({ + extensions: ['.js', '.ts', '.tsx', '.json', '.css'] + }), + commonjs(), + typescript({ tsconfig: './tsconfig.json' }), + terser(), + ], + external: ['@usebruno/lang', 'lodash', 'worker_threads', 'path'] + } +]; \ No newline at end of file diff --git a/packages/bruno-filestore/src/formats/bru/index.ts b/packages/bruno-filestore/src/formats/bru/index.ts new file mode 100644 index 000000000..e71017cdf --- /dev/null +++ b/packages/bruno-filestore/src/formats/bru/index.ts @@ -0,0 +1,203 @@ +import * as _ from 'lodash'; +import { + bruToJsonV2, + jsonToBruV2, + bruToEnvJsonV2, + envJsonToBruV2, + collectionBruToJson as _collectionBruToJson, + jsonToCollectionBru as _jsonToCollectionBru +} from '@usebruno/lang'; + +export const bruRequestToJson = (data: string | any, parsed: boolean = false): any => { + try { + const json = parsed ? data : bruToJsonV2(data); + + let requestType = _.get(json, 'meta.type'); + if (requestType === 'http') { + requestType = 'http-request'; + } else if (requestType === 'graphql') { + requestType = 'graphql-request'; + } else { + requestType = 'http-request'; + } + + const sequence = _.get(json, 'meta.seq'); + const transformedJson = { + type: requestType, + name: _.get(json, 'meta.name'), + seq: !_.isNaN(sequence) ? Number(sequence) : 1, + settings: _.get(json, 'settings', {}), + tags: _.get(json, 'meta.tags', []), + request: { + method: _.upperCase(_.get(json, 'http.method')), + url: _.get(json, 'http.url'), + params: _.get(json, 'params', []), + headers: _.get(json, 'headers', []), + auth: _.get(json, 'auth', {}), + body: _.get(json, 'body', {}), + script: _.get(json, 'script', {}), + vars: _.get(json, 'vars', {}), + assertions: _.get(json, 'assertions', []), + tests: _.get(json, 'tests', ''), + docs: _.get(json, 'docs', '') + } + }; + + transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none'); + transformedJson.request.body.mode = _.get(json, 'http.body', 'none'); + + return transformedJson; + } catch (e) { + return Promise.reject(e); + } +}; + +export const jsonRequestToBru = (json: any): string => { + try { + let type = _.get(json, 'type'); + if (type === 'http-request') { + type = 'http'; + } else if (type === 'graphql-request') { + type = 'graphql'; + } else { + type = 'http'; + } + + const sequence = _.get(json, 'seq'); + const bruJson = { + meta: { + name: _.get(json, 'name'), + type: type, + seq: !_.isNaN(sequence) ? Number(sequence) : 1, + tags: _.get(json, 'tags', []), + }, + http: { + method: _.lowerCase(_.get(json, 'request.method')), + url: _.get(json, 'request.url'), + auth: _.get(json, 'request.auth.mode', 'none'), + body: _.get(json, 'request.body.mode', 'none') + }, + params: _.get(json, 'request.params', []), + headers: _.get(json, 'request.headers', []), + auth: _.get(json, 'request.auth', {}), + body: _.get(json, 'request.body', {}), + script: _.get(json, 'request.script', {}), + vars: { + req: _.get(json, 'request.vars.req', []), + res: _.get(json, 'request.vars.res', []) + }, + assertions: _.get(json, 'request.assertions', []), + tests: _.get(json, 'request.tests', ''), + settings: _.get(json, 'settings', {}), + docs: _.get(json, 'request.docs', '') + }; + + const bru = jsonToBruV2(bruJson); + return bru; + } catch (error) { + throw error; + } +}; + +export const bruCollectionToJson = (data: string | any, parsed: boolean = false): any => { + try { + const json = parsed ? data : _collectionBruToJson(data); + + const transformedJson: any = { + request: { + headers: _.get(json, 'headers', []), + auth: _.get(json, 'auth', {}), + script: _.get(json, 'script', {}), + vars: _.get(json, 'vars', {}), + tests: _.get(json, 'tests', '') + }, + settings: _.get(json, 'settings', {}), + docs: _.get(json, 'docs', '') + }; + + // add meta if it exists + // this is only for folder bru file + if (json.meta) { + transformedJson.meta = { + name: json.meta.name + }; + + // Include seq if it exists + if (json.meta.seq !== undefined) { + const sequence = json.meta.seq; + transformedJson.meta.seq = !isNaN(sequence) ? Number(sequence) : 1; + } + } + + return transformedJson; + } catch (error) { + return Promise.reject(error); + } +}; + +export const jsonCollectionToBru = (json: any, isFolder?: boolean): string => { + try { + const collectionBruJson: any = { + headers: _.get(json, 'request.headers', []), + script: { + req: _.get(json, 'request.script.req', ''), + res: _.get(json, 'request.script.res', '') + }, + vars: { + req: _.get(json, 'request.vars.req', []), + res: _.get(json, 'request.vars.res', []) + }, + tests: _.get(json, 'request.tests', ''), + auth: _.get(json, 'request.auth', {}), + docs: _.get(json, 'docs', '') + }; + + // add meta if it exists + // this is only for folder bru file + if (json?.meta) { + collectionBruJson.meta = { + name: json.meta.name + }; + + // Include seq if it exists + if (json.meta.seq !== undefined) { + const sequence = json.meta.seq; + collectionBruJson.meta.seq = !isNaN(sequence) ? Number(sequence) : 1; + } + } + + if (!isFolder) { + collectionBruJson.auth = _.get(json, 'request.auth', {}); + } + + return _jsonToCollectionBru(collectionBruJson); + } catch (error) { + throw error; + } +}; + +export const bruEnvironmentToJson = (bru: string): any => { + try { + const json = bruToEnvJsonV2(bru); + + // the app env format requires each variable to have a type + // this need to be evaluated and safely removed + // i don't see it being used in schema validation + if (json && json.variables && json.variables.length) { + _.each(json.variables, (v: any) => (v.type = 'text')); + } + + return json; + } catch (error) { + return Promise.reject(error); + } +}; + +export const jsonEnvironmentToBru = (json: any): string => { + try { + const bru = envJsonToBruV2(json); + return bru; + } catch (error) { + throw error; + } +}; \ No newline at end of file diff --git a/packages/bruno-filestore/src/index.ts b/packages/bruno-filestore/src/index.ts new file mode 100644 index 000000000..ba3dfb078 --- /dev/null +++ b/packages/bruno-filestore/src/index.ts @@ -0,0 +1,140 @@ +import { + bruRequestToJson, + jsonRequestToBru, + bruCollectionToJson, + jsonCollectionToBru, + bruEnvironmentToJson, + jsonEnvironmentToBru +} from './formats/bru'; +import { dotenvToJson } from '@usebruno/lang'; +import BruParserWorker from './workers'; +import { + ParseOptions, + StringifyOptions, + ParsedRequest, + ParsedCollection, + ParsedEnvironment +} from './types'; + +export const parseRequest = (content: string, options: ParseOptions = { format: 'bru' }): any => { + if (options.format === 'bru') { + return bruRequestToJson(content); + } + throw new Error(`Unsupported format: ${options.format}`); +}; + +export const stringifyRequest = (requestObj: ParsedRequest, options: StringifyOptions = { format: 'bru' }): string => { + if (options.format === 'bru') { + return jsonRequestToBru(requestObj); + } + throw new Error(`Unsupported format: ${options.format}`); +}; + +let globalWorkerInstance: BruParserWorker | null = null; +let cleanupHandlersRegistered = false; + +const getWorkerInstance = (): BruParserWorker => { + if (!globalWorkerInstance) { + globalWorkerInstance = new BruParserWorker(); + + if (!cleanupHandlersRegistered) { + const cleanup = async () => { + if (globalWorkerInstance) { + await globalWorkerInstance.cleanup(); + globalWorkerInstance = null; + } + }; + + // Handle various exit scenarios + process.on('exit', () => { + // Note: async operations won't work in 'exit' event + // We handle termination in other events + }); + + process.on('SIGINT', async () => { + await cleanup(); + process.exit(0); + }); + + process.on('SIGTERM', async () => { + await cleanup(); + process.exit(0); + }); + + process.on('uncaughtException', async (error) => { + console.error('Uncaught Exception:', error); + await cleanup(); + process.exit(1); + }); + + process.on('unhandledRejection', async (reason) => { + console.error('Unhandled Rejection:', reason); + await cleanup(); + process.exit(1); + }); + + cleanupHandlersRegistered = true; + } + } + return globalWorkerInstance; +}; + +export const parseRequestViaWorker = async (content: string): Promise => { + const fileParserWorker = getWorkerInstance(); + return await fileParserWorker.parseRequest(content); +}; + +export const stringifyRequestViaWorker = async (requestObj: any): Promise => { + const fileParserWorker = getWorkerInstance(); + return await fileParserWorker.stringifyRequest(requestObj); +}; + +export const parseCollection = (content: string, options: ParseOptions = { format: 'bru' }): any => { + if (options.format === 'bru') { + return bruCollectionToJson(content); + } + throw new Error(`Unsupported format: ${options.format}`); +}; + +export const stringifyCollection = (collectionObj: ParsedCollection, options: StringifyOptions = { format: 'bru' }): string => { + if (options.format === 'bru') { + return jsonCollectionToBru(collectionObj, false); + } + throw new Error(`Unsupported format: ${options.format}`); +}; + +export const parseFolder = (content: string, options: ParseOptions = { format: 'bru' }): any => { + if (options.format === 'bru') { + return bruCollectionToJson(content); + } + throw new Error(`Unsupported format: ${options.format}`); +}; + +export const stringifyFolder = (folderObj: any, options: StringifyOptions = { format: 'bru' }): string => { + if (options.format === 'bru') { + return jsonCollectionToBru(folderObj, true); + } + throw new Error(`Unsupported format: ${options.format}`); +}; + +export const parseEnvironment = (content: string, options: ParseOptions = { format: 'bru' }): any => { + if (options.format === 'bru') { + return bruEnvironmentToJson(content); + } + throw new Error(`Unsupported format: ${options.format}`); +}; + +export const stringifyEnvironment = (envObj: ParsedEnvironment, options: StringifyOptions = { format: 'bru' }): string => { + if (options.format === 'bru') { + return jsonEnvironmentToBru(envObj); + } + throw new Error(`Unsupported format: ${options.format}`); +}; + + +export const parseDotEnv = (content: string): Record => { + return dotenvToJson(content); +}; + +export { BruParserWorker }; +export * from './types'; \ No newline at end of file diff --git a/packages/bruno-filestore/src/types.ts b/packages/bruno-filestore/src/types.ts new file mode 100644 index 000000000..6c0564b4e --- /dev/null +++ b/packages/bruno-filestore/src/types.ts @@ -0,0 +1,141 @@ +export interface ParseOptions { + format?: 'bru' | 'yaml'; +} + +export interface StringifyOptions { + format?: 'bru' | 'yaml'; +} + +export interface RequestBody { + mode?: string; + raw?: string; + formUrlEncoded?: Array<{ name: string; value: string; enabled: boolean }>; + multipartForm?: Array<{ name: string; value: string; type: string; enabled: boolean }>; + json?: string; + xml?: string; + sparql?: string; + graphql?: { + query?: string; + variables?: string; + }; +} + +export interface AuthConfig { + mode?: string; + basic?: { + username?: string; + password?: string; + }; + bearer?: { + token?: string; + }; + apikey?: { + key?: string; + value?: string; + placement?: string; + }; + awsv4?: { + accessKeyId?: string; + secretAccessKey?: string; + sessionToken?: string; + service?: string; + region?: string; + profileName?: string; + }; + oauth2?: { + grantType?: string; + callbackUrl?: string; + authorizationUrl?: string; + accessTokenUrl?: string; + clientId?: string; + clientSecret?: string; + scope?: string; + state?: string; + pkce?: boolean; + }; +} + +export interface RequestParam { + name: string; + value: string; + enabled: boolean; +} + +export interface RequestHeader { + name: string; + value: string; + enabled: boolean; +} + +export interface RequestAssertion { + name: string; + value: string; + enabled: boolean; +} + +export interface RequestVars { + req?: Array<{ name: string; value: string; enabled: boolean }>; + res?: Array<{ name: string; value: string; enabled: boolean }>; +} + +export interface RequestScript { + req?: string; + res?: string; +} + +export interface RequestSettings { + [key: string]: any; +} + +export interface RequestData { + method: string; + url: string; + params: RequestParam[]; + headers: RequestHeader[]; + auth: AuthConfig; + body: RequestBody; + script: RequestScript; + vars: RequestVars; + assertions: RequestAssertion[]; + tests: string; + docs: string; +} + +export interface ParsedRequest { + type: 'http-request' | 'graphql-request'; + name: string; + seq: number; + settings: RequestSettings; + tags: string[]; + request: RequestData; +} + +export interface ParsedCollection { + name: string; + type?: string; + version?: string; + [key: string]: any; +} + +export interface EnvironmentVariable { + name: string; + value: string; + enabled: boolean; +} + +export interface ParsedEnvironment { + variables: EnvironmentVariable[]; +} + +export interface WorkerTask { + data: any; + priority: number; + scriptPath: string; + taskType?: 'parse' | 'stringify'; + resolve?: (value: any) => void; + reject?: (reason?: any) => void; +} + +export interface Lane { + maxSize: number; +} \ No newline at end of file diff --git a/packages/bruno-filestore/src/types/bruno-lang.d.ts b/packages/bruno-filestore/src/types/bruno-lang.d.ts new file mode 100644 index 000000000..257d6e8a0 --- /dev/null +++ b/packages/bruno-filestore/src/types/bruno-lang.d.ts @@ -0,0 +1,9 @@ +declare module '@usebruno/lang' { + export function bruToJsonV2(bruContent: string): any; + export function jsonToBruV2(jsonData: any): string; + export function bruToEnvJsonV2(bruContent: string): any; + export function envJsonToBruV2(jsonData: any): string; + export function collectionBruToJson(bruContent: string): any; + export function jsonToCollectionBru(jsonData: any): string; + export function dotenvToJson(envContent: string): Record; +} \ No newline at end of file diff --git a/packages/bruno-filestore/src/workers/WorkerQueue/index.ts b/packages/bruno-filestore/src/workers/WorkerQueue/index.ts new file mode 100644 index 000000000..22bfbaf63 --- /dev/null +++ b/packages/bruno-filestore/src/workers/WorkerQueue/index.ts @@ -0,0 +1,114 @@ +import { Worker } from 'worker_threads'; + +interface QueuedTask { + priority: number; + scriptPath: string; + data: any; + taskType: 'parse' | 'stringify'; + resolve?: (value: any) => void; + reject?: (reason?: any) => void; +} + +class WorkerQueue { + private queue: QueuedTask[]; + private isProcessing: boolean; + private workers: Record; + + constructor() { + this.queue = []; + this.isProcessing = false; + this.workers = {}; + } + + async getWorkerForScriptPath(scriptPath: string) { + if (!this.workers) this.workers = {}; + let worker = this.workers[scriptPath]; + if (!worker || worker.threadId === -1) { + this.workers[scriptPath] = worker = new Worker(scriptPath); + } + return worker; + } + + async enqueue(task: QueuedTask) { + const { priority, scriptPath, data, taskType } = task; + + return new Promise((resolve, reject) => { + this.queue.push({ priority, scriptPath, data, taskType, resolve, reject }); + this.queue?.sort((taskX, taskY) => taskX?.priority - taskY?.priority); + this.processQueue(); + }); + } + + async processQueue() { + if (this.isProcessing || this.queue.length === 0){ + return; + } + + this.isProcessing = true; + const { scriptPath, data, taskType, resolve, reject } = this.queue.shift() as QueuedTask; + + try { + const result = await this.runWorker({ scriptPath, data, taskType }); + resolve?.(result); + } catch (error) { + reject?.(error); + } finally { + this.isProcessing = false; + this.processQueue(); + } + } + + async runWorker({ scriptPath, data, taskType }: { scriptPath: string; data: any; taskType: 'parse' | 'stringify' }) { + return new Promise(async (resolve, reject) => { + let worker = await this.getWorkerForScriptPath(scriptPath); + + const messageHandler = (data: any) => { + worker.off('message', messageHandler); + worker.off('error', errorHandler); + worker.off('exit', exitHandler); + + if (data?.error) { + reject(new Error(data?.error)); + } else { + resolve(data); + } + }; + + const errorHandler = (error: Error) => { + worker.off('message', messageHandler); + worker.off('error', errorHandler); + worker.off('exit', exitHandler); + reject(error); + }; + + const exitHandler = (code: number) => { + worker.off('message', messageHandler); + worker.off('error', errorHandler); + worker.off('exit', exitHandler); + // Remove dead worker from cache + delete this.workers[scriptPath]; + reject(new Error(`Worker stopped with exit code ${code}`)); + }; + + worker.on('message', messageHandler); + worker.on('error', errorHandler); + worker.on('exit', exitHandler); + + worker.postMessage({ taskType, data }); + }); + } + + async cleanup() { + const promises = Object.values(this.workers).map(worker => { + if (worker.threadId !== -1) { + return worker.terminate(); + } + return Promise.resolve(); + }); + + await Promise.allSettled(promises); + this.workers = {}; + } +} + +export default WorkerQueue; \ No newline at end of file diff --git a/packages/bruno-filestore/src/workers/index.ts b/packages/bruno-filestore/src/workers/index.ts new file mode 100644 index 000000000..c6a876f1e --- /dev/null +++ b/packages/bruno-filestore/src/workers/index.ts @@ -0,0 +1,86 @@ +import WorkerQueue from "./WorkerQueue"; +import { Lane } from "../types"; +import path from "path"; + +const sizeInMB = (size: number): number => { + return size / (1024 * 1024); +} + +const getSize = (data: any): number => { + return sizeInMB(typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8')); +} + +/** + * Lanes are used to determine which worker queue to use based on the size of the data. + * + * The first lane is for smaller files (<0.1MB), the second lane is for larger files (>=0.1MB). + * This helps with parsing performance. + */ +const LANES: Lane[] = [{ + maxSize: 0.005 +},{ + maxSize: 0.1 +},{ + maxSize: 1 +},{ + maxSize: 10 +},{ + maxSize: 100 +}]; + +interface WorkerQueueWithSize { + maxSize: number; + workerQueue: WorkerQueue; + +} + +class BruParserWorker { + private workerQueues: WorkerQueueWithSize[]; + + constructor() { + this.workerQueues = LANES?.map(lane => ({ + maxSize: lane?.maxSize, + workerQueue: new WorkerQueue() + })); + } + + private getWorkerQueue(size: number): WorkerQueue { + // Find the first queue that can handle the given size + // or fallback to the last queue for largest files + const queueForSize = this.workerQueues.find((queue) => + queue.maxSize >= size + ); + + return queueForSize?.workerQueue ?? this.workerQueues[this.workerQueues.length - 1].workerQueue; + } + + private async enqueueTask({ data, taskType }: { data: any; taskType: 'parse' | 'stringify' }): Promise { + const size = getSize(data); + const workerQueue = this.getWorkerQueue(size); + const workerScriptPath = path.join(__dirname, './workers/worker-script.js'); + + return workerQueue.enqueue({ + data, + priority: size, + scriptPath: workerScriptPath, + taskType, + }); + } + + async parseRequest(data: any): Promise { + return this.enqueueTask({ data, taskType: 'parse' }); + } + + async stringifyRequest(data: any): Promise { + return this.enqueueTask({ data, taskType: 'stringify' }); + } + + async cleanup(): Promise { + const cleanupPromises = this.workerQueues.map(({ workerQueue }) => + workerQueue.cleanup() + ); + await Promise.allSettled(cleanupPromises); + } +} + +export default BruParserWorker; \ No newline at end of file diff --git a/packages/bruno-filestore/src/workers/worker-script.ts b/packages/bruno-filestore/src/workers/worker-script.ts new file mode 100644 index 000000000..e41fc974a --- /dev/null +++ b/packages/bruno-filestore/src/workers/worker-script.ts @@ -0,0 +1,27 @@ +import { parentPort } from 'worker_threads'; +import { bruRequestToJson, jsonRequestToBru } from '../formats/bru'; + +interface WorkerMessage { + taskType: 'parse' | 'stringify'; + data: any; +} + +parentPort?.on('message', async (message: WorkerMessage) => { + try { + const { taskType, data } = message; + let result: any; + + if (taskType === 'parse') { + result = bruRequestToJson(data); + } else if (taskType === 'stringify') { + result = jsonRequestToBru(data); + } else { + throw new Error(`Unknown task type: ${taskType}`); + } + + parentPort?.postMessage(result); + } catch (error: any) { + console.error('Worker error:', error); + parentPort?.postMessage({ error: error?.message }); + } +}); \ No newline at end of file diff --git a/packages/bruno-filestore/tsconfig.json b/packages/bruno-filestore/tsconfig.json new file mode 100644 index 000000000..7c584c379 --- /dev/null +++ b/packages/bruno-filestore/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "moduleResolution": "node", + "declaration": true, + "declarationDir": "./dist/types", + "allowJs": true, + "checkJs": false, + "typeRoots": ["./node_modules/@types", "./src/types"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.d.ts"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/scripts/setup.js b/scripts/setup.js index fc8b67a6b..e0a15bdc1 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -76,6 +76,7 @@ async function setup() { execCommand('npm run build:bruno-common', 'Building bruno-common'); execCommand('npm run build:bruno-converters', 'Building bruno-converters'); execCommand('npm run build:bruno-requests', 'Building bruno-requests'); + execCommand('npm run build:bruno-filestore', 'Building bruno-filestore'); // Bundle JS sandbox libraries execCommand( From 0fec0003f28b27f276c178d228306776213d7205 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Thu, 24 Jul 2025 20:44:13 +0530 Subject: [PATCH 02/30] fix: always showing scrollbar (#5184) --- .../bruno-app/src/components/CollectionSettings/Docs/index.js | 2 +- packages/bruno-app/src/components/CollectionSettings/index.js | 4 ++-- packages/bruno-app/src/components/FolderSettings/index.js | 4 ++-- packages/bruno-app/src/components/RequestPane/Auth/index.js | 2 +- packages/bruno-app/src/components/ResponsePane/index.js | 2 +- .../src/components/RunnerResults/ResponsePane/index.js | 4 ++-- packages/bruno-app/src/components/RunnerResults/index.jsx | 2 +- packages/bruno-app/src/components/VariablesEditor/index.js | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js index 5298ac5ff..3289e099c 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js @@ -46,7 +46,7 @@ const Docs = ({ collection }) => { } return ( - +
diff --git a/packages/bruno-app/src/components/CollectionSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/index.js index ef220351a..92ee2d2c6 100644 --- a/packages/bruno-app/src/components/CollectionSettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/index.js @@ -132,7 +132,7 @@ const CollectionSettings = ({ collection }) => { }; return ( - +
setTab('overview')}> Overview @@ -169,7 +169,7 @@ const CollectionSettings = ({ collection }) => { {clientCertConfig.length > 0 && }
-
{getTabPanel(tab)}
+
{getTabPanel(tab)}
); }; diff --git a/packages/bruno-app/src/components/FolderSettings/index.js b/packages/bruno-app/src/components/FolderSettings/index.js index abd13614a..61170e5ca 100644 --- a/packages/bruno-app/src/components/FolderSettings/index.js +++ b/packages/bruno-app/src/components/FolderSettings/index.js @@ -74,7 +74,7 @@ const FolderSettings = ({ collection, folder }) => { }; return ( - +
setTab('headers')}> @@ -101,7 +101,7 @@ const FolderSettings = ({ collection, folder }) => { Docs
-
{getTabPanel(tab)}
+
{getTabPanel(tab)}
); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index c16f7bb68..7ccbc3e7d 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -109,7 +109,7 @@ const Auth = ({ item, collection }) => { }; return ( - +
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 22955ac2d..d9147632a 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -176,7 +176,7 @@ const ResponsePane = ({ item, collection }) => { ) : null}
{ } return ( - +
selectTab('response')}> Response @@ -128,7 +128,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
-
+
{hasScriptError && showScriptErrorCard && ( +
Runner diff --git a/packages/bruno-app/src/components/VariablesEditor/index.js b/packages/bruno-app/src/components/VariablesEditor/index.js index b601f3de0..edde0e20c 100644 --- a/packages/bruno-app/src/components/VariablesEditor/index.js +++ b/packages/bruno-app/src/components/VariablesEditor/index.js @@ -89,7 +89,7 @@ const VariablesEditor = ({ collection }) => { const reactInspectorTheme = storedTheme === 'light' ? 'chromeLight' : 'chromeDark'; return ( - + From 6daaf9066708a257ae14b3ace10679d7b8140fca Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Fri, 25 Jul 2025 14:05:28 +0530 Subject: [PATCH 03/30] feat: update statusbar styling, enhance cookie button accessibility, and adjust theme colors (#5185) Co-authored-by: Maintainer Bruno --- .../src/components/StatusBar/StyledWrapper.js | 20 +++------ .../src/components/StatusBar/index.js | 41 ++++++++++--------- packages/bruno-app/src/themes/dark.js | 1 + packages/bruno-app/src/themes/light.js | 1 + 4 files changed, 29 insertions(+), 34 deletions(-) diff --git a/packages/bruno-app/src/components/StatusBar/StyledWrapper.js b/packages/bruno-app/src/components/StatusBar/StyledWrapper.js index 930753319..4304f37dc 100644 --- a/packages/bruno-app/src/components/StatusBar/StyledWrapper.js +++ b/packages/bruno-app/src/components/StatusBar/StyledWrapper.js @@ -5,12 +5,12 @@ const StyledWrapper = styled.div` display: flex; align-items: center; justify-content: space-between; - padding: 0 16px; - height: 22px; + padding: 0 1rem; + height: 1.5rem; background: ${(props) => props.theme.sidebar.bg}; border-top: 1px solid ${(props) => props.theme.statusBar.border}; - color: ${(props) => props.theme.sidebar.color}; - font-size: 12px; + color: ${(props) => props.theme.statusBar.color}; + font-size: 0.75rem; user-select: none; position: relative; } @@ -32,9 +32,7 @@ const StyledWrapper = styled.div` align-items: center; justify-content: center; padding: 0 4px; - color: ${(props) => props.theme.sidebar.color}; cursor: pointer; - opacity: 0.7; position: relative; outline: none; } @@ -43,13 +41,11 @@ const StyledWrapper = styled.div` display: flex; align-items: center; justify-content: center; - gap: 6px; + gap: 0.25rem; position: relative; } .console-label { - font-size: 11px; - font-weight: 500; white-space: nowrap; } @@ -66,17 +62,13 @@ const StyledWrapper = styled.div` width: 1px; height: 16px; background: ${(props) => props.theme.sidebar.dragbar}; - margin: 0 8px; - opacity: 0.3; + opacity: 0.4; } .status-bar-version { display: flex; align-items: center; padding: 2px 6px; - font-size: 10px; - color: ${(props) => props.theme.sidebar.muted}; - font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; } `; diff --git a/packages/bruno-app/src/components/StatusBar/index.js b/packages/bruno-app/src/components/StatusBar/index.js index fc4718bbb..64f7c8de0 100644 --- a/packages/bruno-app/src/components/StatusBar/index.js +++ b/packages/bruno-app/src/components/StatusBar/index.js @@ -71,18 +71,6 @@ const StatusBar = () => { - - - -
@@ -92,7 +80,20 @@ const StatusBar = () => {
-
+
+ +
diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index f56217e7a..00a42cb66 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -299,6 +299,7 @@ const darkTheme = { statusBar: { border: '#323233', + color: 'rgb(169, 169, 169)' }, console: { bg: '#1e1e1e', diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index a012fdd31..ef3cd135b 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -300,6 +300,7 @@ const lightTheme = { statusBar: { border: '#E9E9E9', + color: 'rgb(100, 100, 100)' }, console: { bg: '#f8f9fa', From 63f5108dfd9af074d37101fcc36b0847976f020b Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 25 Jul 2025 14:07:38 +0530 Subject: [PATCH 04/30] list block grammar fixes (#5180) --- packages/bruno-lang/v2/src/bruToJson.js | 11 +- packages/bruno-lang/v2/tests/list.spec.js | 798 ++++++++++++++++++++++ 2 files changed, 803 insertions(+), 6 deletions(-) create mode 100644 packages/bruno-lang/v2/tests/list.spec.js diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 3ce411841..46738c86d 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -67,10 +67,9 @@ const grammar = ohm.grammar(`Bru { textchar = ~nl any // List - listend = stnl* "]" - list = st* "[" listitems? listend - listitems = (~listend stnl)* listitem (~listend stnl* listitem)* (~listend space)* - listitem = st* textchar+ st* + list = st* "[" nl+ listitems? st* nl+ st* "]" + listitems = listitem (nl+ listitem)* + listitem = st+ (alnum | "_" | "-")+ st* meta = "meta" dictionary settings = "settings" dictionary @@ -318,10 +317,10 @@ const sem = grammar.createSemantics().addAttribute('ast', { assertkey(chars) { return chars.sourceString ? chars.sourceString.trim() : ''; }, - list(_1, _2, listitems, _3) { + list(_1, _2, _3, listitems, _4, _5, _6, _7) { return listitems.ast.flat() }, - listitems(_1, listitem, _2, rest, _3) { + listitems(listitem, _1, rest) { return [listitem.ast, ...rest.ast] }, listitem(_1, textchar, _2) { diff --git a/packages/bruno-lang/v2/tests/list.spec.js b/packages/bruno-lang/v2/tests/list.spec.js new file mode 100644 index 000000000..19804779f --- /dev/null +++ b/packages/bruno-lang/v2/tests/list.spec.js @@ -0,0 +1,798 @@ +/** + * This test file is used to test list parsing in various BruFile blocks. + */ +const parser = require('../src/bruToJson'); + +describe('List Support in BruFile Blocks', () => { + + describe('Basic List Functionality', () => { + describe('Valid List Syntax', () => { + it('should parse simple list with proper indentation', () => { + const input = ` +meta { + tags: [ + tag_1 + tag_2 + ] +} +`; + const output = parser(input); + const expected = { + meta: { + seq: 1, + tags: ['tag_1', 'tag_2'], + type: "http" + } + }; + expect(output).toEqual(expected); + }); + + it('should parse list with mixed properties', () => { + const input = ` +meta { + name: request_name + tags: [ + regression + smoke_test + ] + type: http +} +`; + const output = parser(input); + const expected = { + meta: { + seq: 1, + name: "request_name", + tags: ['regression', 'smoke_test'], + type: "http" + } + }; + expect(output).toEqual(expected); + }); + + it('should parse list with varying indentation inside list', () => { + const input = ` +meta { + tags: [ + tag_1 + tag_2 + tag_3 + ] +} +`; + const output = parser(input); + const expected = { + meta: { + seq: 1, + tags: ['tag_1', 'tag_2', 'tag_3'], + type: "http" + } + }; + expect(output).toEqual(expected); + }); + + it('should parse list with alphanumeric, underscore, and hyphen characters', () => { + const input = ` +meta { + tags: [ + tag-with-hyphens + tag_with_underscores + tag123numbers + CamelCaseTag + ] +} +`; + const output = parser(input); + const expected = { + meta: { + seq: 1, + tags: ['tag-with-hyphens', 'tag_with_underscores', 'tag123numbers', 'CamelCaseTag'], + type: "http" + } + }; + expect(output).toEqual(expected); + }); + }); + + describe('Invalid List Syntax', () => { + it('should fail when list items have no indentation', () => { + const input = ` +meta { + tags: [ + tag_1 +tag_2 + ] +} +`; + expect(() => parser(input)).toThrow(); + }); + + it('should fail when list has empty lines between items', () => { + const input = ` +meta { + tags: [ + tag_1 + + tag_2 + ] +} +`; + expect(() => parser(input)).toThrow(); + }); + + it('should fail when list opening bracket is on same line as first item', () => { + const input = ` +meta { + tags: [tag_1 + tag_2 + ] +} +`; + expect(() => parser(input)).toThrow(); + }); + + it('should fail when list closing bracket is on same line as last item', () => { + const input = ` +meta { + tags: [ + tag_1 + tag_2] +} +`; + expect(() => parser(input)).toThrow(); + }); + + it('should fail when list items contain invalid characters - variation 1', () => { + const input = ` +meta { + tags: [ + tag*1 + tag@2 + ] +} +`; + expect(() => parser(input)).toThrow(); + }); + + it('should fail when list items contain spaces', () => { + const input = ` +meta { + tags: [ + tag with spaces + another-tag + ] +} +`; + expect(() => parser(input)).toThrow(); + }); + + it('should fail when list items contain invalid characters - variation 2', () => { + const input = ` +meta { + tags: [ + tag_1, + tag_2 + ] +} +`; + expect(() => parser(input)).toThrow(); + }); + + it('should fail when first list item has no indentation', () => { + const input = ` +meta { + tags: [ tag_1 + tag_2 + ] +} +`; + expect(() => parser(input)).toThrow(); + }); + + it('should fail when list item are not seperated by atleast one newline', () => { + const input = ` +meta { + tags: [ + tag_1 + tag_2 tag_3 + ] +} +`; + expect(() => parser(input)).toThrow(); + }); + + it('should not parse empty list', () => { + const input = ` +meta { + tags: [ + ] +} +`; + + expect(() => parser(input)).toThrow(); + }); + }); + + describe('String Values That Look Like Lists', () => { + it('should parse inline bracketed strings as regular values', () => { + const input = ` +meta { + name: [some name] + tags: [ + actual_list_item + ] +} +`; + const output = parser(input); + const expected = { + meta: { + seq: 1, + name: "[some name]", + tags: ['actual_list_item'], + type: "http" + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bracketed strings with spaces as regular values', () => { + const input = ` +meta { + name: [ this is the name ] + tags: [ + tag_1 + tag_2 + ] +} +`; + const output = parser(input); + const expected = { + meta: { + seq: 1, + name: "[ this is the name ]", + tags: ['tag_1', 'tag_2'], + type: "http" + } + }; + expect(output).toEqual(expected); + }); + + it('should fail when multiline bracketed strings are malformed', () => { + const input = ` +meta { + name: [this spans + multiple lines + ] +} +`; + expect(() => parser(input)).toThrow(); + }); + }); + }); + + describe('Lists in Meta Block', () => { + it('should parse tags in meta block', () => { + const input = ` +meta { + name: API Test + tags: [ + api + integration + v1 + ] +} +`; + const output = parser(input); + const expected = { + meta: { + name: "API Test", + tags: ['api', 'integration', 'v1'], + seq: 1, + type: "http" + } + }; + expect(output).toEqual(expected); + }); + + it('should parse custom list properties in meta block', () => { + const input = ` +meta { + categories: [ + user-management + auth + ] + environments: [ + staging + production + ] +} +`; + const output = parser(input); + const expected = { + meta: { + seq: 1, + categories: ['user-management', 'auth'], + environments: ['staging', 'production'], + type: "http" + } + }; + expect(output).toEqual(expected); + }); + }); + + describe('Lists type content in Body Blocks', () => { + it('should parse bru file with a text body block that has list type values - variation 1', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:text { + meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] + } +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + text: `meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +}` + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bru file with a text body block that has list type values - variation 2', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:text { + meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] + } +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + text: `meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +}` + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bru file with a json body block that has list type values - variation 1', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:json { + meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] + } +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + json: `meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +}` + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bru file with a json body block that has list type values - variation 2', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:json { + meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] + } +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + json: `meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +}` + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bru file with a json body block that has array values', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:json { + { + array: [ + "1", + "2", + "3" + ] + } +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + json: `{ + array: [ + "1", + "2", + "3" + ] +}` + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bru file with a json body block that has array of objects - variation 1', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:json { + { + array: [ + { + "id": 1 + }, + { + "id": 2 + }, + { + "id": 3 + } + ] + } +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + json: `{ + array: [ + { + "id": 1 + }, + { + "id": 2 + }, + { + "id": 3 + } + ] +}` + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bru file with a json body block that has array of objects - variation 2', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:json { + [{ + "foo": "bar" + }] +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + json: `[{ + "foo": "bar" +}]` + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bru file with a json body block that has array of objects - variation 3', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:json { + [{"foo": "bar"}] +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + json: `[{"foo": "bar"}]` + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bru file with a json body block that has objects and arrays - variation 1', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:json { + { + object: { + array: [ + { + "id": 1 + }, + { + "id": 2 + }, + { + "id": 3 + } + ] + } + } +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + json: `{ + object: { + array: [ + { + "id": 1 + }, + { + "id": 2 + }, + { + "id": 3 + } + ] + } +}` + } + }; + expect(output).toEqual(expected); + }); + + it('should parse bru file with a json body block with complex arrays', () => { + const input = ` +meta { + name: [name] + tags: [ + tag_1 + tag_2 + ] +} +body:json { + [ + "string", + array: [ + "tag_1", + "tag_2" + ], + object: { + array: [ + { + "id": 1 + }, + { + "id": 2 + }, + { + "id": 3 + } + ] + } + ] +} +`; + const output = parser(input); + + const expected = { + meta: { + name: "[name]", + tags: [ + "tag_1", + "tag_2" + ], + seq: 1, + type: 'http' + }, + body: { + json: `[ + "string", + array: [ + "tag_1", + "tag_2" + ], + object: { + array: [ + { + "id": 1 + }, + { + "id": 2 + }, + { + "id": 3 + } + ] + } +]` + } + }; + expect(output).toEqual(expected); + }); + }); +}); \ No newline at end of file From 78b8b7f6e47eb8f95f92d9957608574c62e14613 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Fri, 25 Jul 2025 17:13:58 +0530 Subject: [PATCH 05/30] Revert "disable ssl/tls & enable system proxy (#5125)" (#5196) This reverts commit 36e3554d5f635cc3fc024c1ccce44b250574084a. --- packages/bruno-app/src/providers/ReduxStore/slices/app.js | 2 +- packages/bruno-electron/src/store/preferences.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/app.js b/packages/bruno-app/src/providers/ReduxStore/slices/app.js index 6cb7f541b..900cf24b6 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/app.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/app.js @@ -11,7 +11,7 @@ const initialState = { isEnvironmentSettingsModalOpen: false, preferences: { request: { - sslVerification: false, + sslVerification: true, customCaCertificate: { enabled: false, filePath: null diff --git a/packages/bruno-electron/src/store/preferences.js b/packages/bruno-electron/src/store/preferences.js index d3ab2941c..f5dac56d5 100644 --- a/packages/bruno-electron/src/store/preferences.js +++ b/packages/bruno-electron/src/store/preferences.js @@ -10,7 +10,7 @@ const { get, merge } = require('lodash'); const defaultPreferences = { request: { - sslVerification: false, + sslVerification: true, customCaCertificate: { enabled: false, filePath: null @@ -27,7 +27,7 @@ const defaultPreferences = { codeFontSize: 14 }, proxy: { - mode: 'system', + mode: 'off', protocol: 'http', hostname: '', port: null, @@ -132,7 +132,7 @@ const savePreferences = async (newPreferences) => { const preferencesUtil = { shouldVerifyTls: () => { - return get(getPreferences(), 'request.sslVerification', false); + return get(getPreferences(), 'request.sslVerification', true); }, shouldUseCustomCaCertificate: () => { return get(getPreferences(), 'request.customCaCertificate.enabled', false); From 29e6470f7ab6c09e24e370a62b29f3856fabb907 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Fri, 25 Jul 2025 18:05:10 +0530 Subject: [PATCH 06/30] fix: insecure requests not working (#5197) --- packages/bruno-cli/src/runner/run-single-request.js | 4 ++++ packages/bruno-electron/src/utils/proxy-util.js | 3 +++ 2 files changed, 7 insertions(+) diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index e8ecb4d61..55397c7c4 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -287,6 +287,10 @@ const runSingleRequest = async function ( https_proxy, Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined ); + } else { + request.httpsAgent = new https.Agent({ + ...httpsAgentRequestFields + }); } } catch (error) { throw new Error('Invalid system https_proxy'); diff --git a/packages/bruno-electron/src/utils/proxy-util.js b/packages/bruno-electron/src/utils/proxy-util.js index 2a9ef26cb..f611ea1f6 100644 --- a/packages/bruno-electron/src/utils/proxy-util.js +++ b/packages/bruno-electron/src/utils/proxy-util.js @@ -368,6 +368,9 @@ function setupProxyAgents({ { proxy: https_proxy,...tlsOptions }, timeline ); + } else { + const TimelineHttpsAgent = createTimelineAgentClass(https.Agent); + requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline); } } catch (error) { throw new Error('Invalid system https_proxy'); From 780beb832ea33975f21ba012af42e4a4c8d76d18 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Tue, 29 Jul 2025 12:11:50 +0530 Subject: [PATCH 07/30] fix: typescript errors (#5214) --- package-lock.json | 192 ++++++++++-------- packages/bruno-filestore/package.json | 3 +- packages/bruno-filestore/src/index.ts | 6 +- .../src/workers/WorkerQueue/index.ts | 2 +- packages/bruno-filestore/src/workers/index.ts | 6 +- .../src/workers/worker-script.ts | 4 +- packages/bruno-filestore/tsconfig.json | 2 + 7 files changed, 116 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce045a5c3..f3c68d89c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1603,7 +1603,7 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1621,7 +1621,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1638,7 +1638,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1656,7 +1656,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@babel/helper-member-expression-to-functions": { @@ -1727,7 +1727,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1802,7 +1802,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -1845,7 +1845,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1862,7 +1862,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1878,7 +1878,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1894,7 +1894,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1912,7 +1912,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1947,7 +1947,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2046,7 +2046,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2062,7 +2062,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2244,7 +2244,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -2261,7 +2261,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2277,7 +2277,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2295,7 +2295,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -2313,7 +2313,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2329,7 +2329,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2361,7 +2361,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", @@ -2378,7 +2378,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -2399,7 +2399,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -2409,7 +2409,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2426,7 +2426,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2442,7 +2442,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2459,7 +2459,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2475,7 +2475,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2492,7 +2492,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2508,7 +2508,7 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2524,7 +2524,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2556,7 +2556,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2573,7 +2573,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -2591,7 +2591,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2607,7 +2607,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2623,7 +2623,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2639,7 +2639,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2655,7 +2655,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2688,7 +2688,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2707,7 +2707,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2724,7 +2724,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2741,7 +2741,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2772,7 +2772,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2788,7 +2788,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -2806,7 +2806,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2823,7 +2823,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2855,7 +2855,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2887,7 +2887,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -2905,7 +2905,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2990,7 +2990,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -3007,7 +3007,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3024,7 +3024,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3040,7 +3040,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3056,7 +3056,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -3073,7 +3073,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3089,7 +3089,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3105,7 +3105,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3140,7 +3140,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3156,7 +3156,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3173,7 +3173,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3190,7 +3190,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3207,7 +3207,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.26.0", @@ -3308,7 +3308,7 @@ "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -8524,7 +8524,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { @@ -8547,7 +8546,6 @@ "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/linkify-it": "*", @@ -8558,7 +8556,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -9916,7 +9913,7 @@ "version": "0.4.12", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", @@ -9931,7 +9928,7 @@ "version": "0.10.6", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.2", @@ -9945,7 +9942,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3" @@ -12015,7 +12012,7 @@ "version": "3.39.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "browserslist": "^4.24.2" @@ -13752,7 +13749,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14350,7 +14346,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -17031,7 +17027,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -19458,7 +19454,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.flow": { @@ -21478,7 +21474,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -23747,14 +23743,14 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -23773,7 +23769,7 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" @@ -23804,7 +23800,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -23822,14 +23818,14 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/regjsparser": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.0.2" @@ -23842,7 +23838,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -24016,7 +24012,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -26297,7 +26293,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -27119,7 +27115,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -27181,7 +27177,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -27191,7 +27187,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -27205,7 +27201,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -27215,7 +27211,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -32826,6 +32822,7 @@ "@rollup/plugin-typescript": "^9.0.2", "@types/jest": "^29.5.11", "@types/lodash": "^4.14.191", + "@types/node": "^24.1.0", "babel-jest": "^29.7.0", "jest": "^29.2.0", "rimraf": "^3.0.2", @@ -32836,6 +32833,16 @@ "typescript": "^4.8.4" } }, + "packages/bruno-filestore/node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, "packages/bruno-filestore/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -32899,6 +32906,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/bruno-filestore/node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, + "license": "MIT" + }, "packages/bruno-graphql-docs": { "name": "@usebruno/graphql-docs", "version": "0.1.0", diff --git a/packages/bruno-filestore/package.json b/packages/bruno-filestore/package.json index 69558339b..edf79b6f1 100644 --- a/packages/bruno-filestore/package.json +++ b/packages/bruno-filestore/package.json @@ -27,6 +27,7 @@ "@rollup/plugin-typescript": "^9.0.2", "@types/jest": "^29.5.11", "@types/lodash": "^4.14.191", + "@types/node": "^24.1.0", "babel-jest": "^29.7.0", "jest": "^29.2.0", "rimraf": "^3.0.2", @@ -43,4 +44,4 @@ "@usebruno/lang": "0.12.0", "lodash": "^4.17.21" } -} \ No newline at end of file +} \ No newline at end of file diff --git a/packages/bruno-filestore/src/index.ts b/packages/bruno-filestore/src/index.ts index ba3dfb078..a28afa618 100644 --- a/packages/bruno-filestore/src/index.ts +++ b/packages/bruno-filestore/src/index.ts @@ -61,13 +61,13 @@ const getWorkerInstance = (): BruParserWorker => { process.exit(0); }); - process.on('uncaughtException', async (error) => { + process.on('uncaughtException', async (error: Error) => { console.error('Uncaught Exception:', error); await cleanup(); process.exit(1); }); - process.on('unhandledRejection', async (reason) => { + process.on('unhandledRejection', async (reason: unknown) => { console.error('Unhandled Rejection:', reason); await cleanup(); process.exit(1); @@ -137,4 +137,4 @@ export const parseDotEnv = (content: string): Record => { }; export { BruParserWorker }; -export * from './types'; \ No newline at end of file +export * from './types'; \ No newline at end of file diff --git a/packages/bruno-filestore/src/workers/WorkerQueue/index.ts b/packages/bruno-filestore/src/workers/WorkerQueue/index.ts index 22bfbaf63..bb248ee3f 100644 --- a/packages/bruno-filestore/src/workers/WorkerQueue/index.ts +++ b/packages/bruno-filestore/src/workers/WorkerQueue/index.ts @@ -1,4 +1,4 @@ -import { Worker } from 'worker_threads'; +import { Worker } from 'node:worker_threads'; interface QueuedTask { priority: number; diff --git a/packages/bruno-filestore/src/workers/index.ts b/packages/bruno-filestore/src/workers/index.ts index c6a876f1e..af66ea107 100644 --- a/packages/bruno-filestore/src/workers/index.ts +++ b/packages/bruno-filestore/src/workers/index.ts @@ -1,6 +1,6 @@ -import WorkerQueue from "./WorkerQueue"; -import { Lane } from "../types"; -import path from "path"; +import WorkerQueue from './WorkerQueue'; +import { Lane } from '../types'; +import path from 'node:path'; const sizeInMB = (size: number): number => { return size / (1024 * 1024); diff --git a/packages/bruno-filestore/src/workers/worker-script.ts b/packages/bruno-filestore/src/workers/worker-script.ts index e41fc974a..7a6529aab 100644 --- a/packages/bruno-filestore/src/workers/worker-script.ts +++ b/packages/bruno-filestore/src/workers/worker-script.ts @@ -1,4 +1,4 @@ -import { parentPort } from 'worker_threads'; +import { parentPort } from 'node:worker_threads'; import { bruRequestToJson, jsonRequestToBru } from '../formats/bru'; interface WorkerMessage { @@ -24,4 +24,4 @@ parentPort?.on('message', async (message: WorkerMessage) => { console.error('Worker error:', error); parentPort?.postMessage({ error: error?.message }); } -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/bruno-filestore/tsconfig.json b/packages/bruno-filestore/tsconfig.json index 7c584c379..22385164b 100644 --- a/packages/bruno-filestore/tsconfig.json +++ b/packages/bruno-filestore/tsconfig.json @@ -15,6 +15,8 @@ "declarationDir": "./dist/types", "allowJs": true, "checkJs": false, + "types": ["node"], + "lib": ["ES2020"], "typeRoots": ["./node_modules/@types", "./src/types"] }, "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.d.ts"], From 62151330f2b1f5a52fa6d77998d977ed8e4842ad Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Tue, 29 Jul 2025 17:15:30 +0530 Subject: [PATCH 08/30] Fix: Loading state while collection mount (#5138) --- .../src/components/RunnerResults/index.jsx | 38 ++++++-- .../src/components/ShareCollection/index.js | 44 +++++++-- .../Sidebar/Collections/Collection/index.js | 15 +-- .../src/providers/App/useIpcEvents.js | 7 +- .../ReduxStore/slices/collections/index.js | 7 ++ .../bruno-app/src/utils/collections/index.js | 4 + .../src/app/collection-watcher.js | 97 ++++++++++++++++++- packages/bruno-electron/src/ipc/collection.js | 4 +- 8 files changed, 185 insertions(+), 31 deletions(-) diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index 736589f9c..a5dc83a1a 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -2,10 +2,10 @@ import React, { useState, useRef, useEffect } from 'react'; import path from 'utils/common/path'; import { useDispatch } from 'react-redux'; import { get, cloneDeep } from 'lodash'; -import { runCollectionFolder, cancelRunnerExecution } from 'providers/ReduxStore/slices/collections/actions'; +import { runCollectionFolder, cancelRunnerExecution, mountCollection } from 'providers/ReduxStore/slices/collections/actions'; import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections'; import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections'; -import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun } from '@tabler/icons'; +import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconLoader2 } from '@tabler/icons'; import ResponsePane from './ResponsePane'; import StyledWrapper from './StyledWrapper'; import { areItemsLoading } from 'utils/collections'; @@ -103,11 +103,24 @@ export default function RunnerResults({ collection }) { }) .filter(Boolean); + const ensureCollectionIsMounted = () => { + if(collection.mountStatus === 'mounted'){ + return; + } + dispatch(mountCollection({ + collectionUid: collection.uid, + collectionPathname: collection.pathname, + brunoConfig: collection.brunoConfig + })); + }; + const runCollection = () => { + ensureCollectionIsMounted(); dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags)); }; const runAgain = () => { + ensureCollectionIsMounted(); dispatch( runCollectionFolder( collection.uid, @@ -149,8 +162,12 @@ export default function RunnerResults({ collection }) {
You have {totalRequestsInCollection} requests in this collection. + {isCollectionLoading && ( + + (Loading...) + + )}
- {isCollectionLoading ?
Requests in this collection are still loading.
: null}
- +
+ - + +
); } diff --git a/packages/bruno-app/src/components/ShareCollection/index.js b/packages/bruno-app/src/components/ShareCollection/index.js index d0db00905..6b18ae837 100644 --- a/packages/bruno-app/src/components/ShareCollection/index.js +++ b/packages/bruno-app/src/components/ShareCollection/index.js @@ -1,6 +1,6 @@ import React from 'react'; import Modal from 'components/Modal'; -import { IconDownload } from '@tabler/icons'; +import { IconDownload, IconLoader2 } from '@tabler/icons'; import StyledWrapper from './StyledWrapper'; import Bruno from 'components/Bruno'; import exportBrunoCollection from 'utils/collections/export'; @@ -8,10 +8,12 @@ import exportPostmanCollection from 'utils/exporters/postman-collection'; import { cloneDeep } from 'lodash'; import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index'; import { useSelector } from 'react-redux'; -import { findCollectionByUid } from 'utils/collections/index'; +import { findCollectionByUid, areItemsLoading } from 'utils/collections/index'; const ShareCollection = ({ onClose, collectionUid }) => { const collection = useSelector(state => findCollectionByUid(state.collections.collections, collectionUid)); + const isCollectionLoading = areItemsLoading(collection); + const handleExportBrunoCollection = () => { const collectionCopy = cloneDeep(collection); exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy)); @@ -35,23 +37,49 @@ const ShareCollection = ({ onClose, collectionUid }) => { >
-
+
- + {isCollectionLoading ? ( + + ) : ( + + )}
Bruno Collection
-
Export in Bruno format
+
+ {isCollectionLoading ? 'Loading collection...' : 'Export in Bruno format'} +
-
+
- + {isCollectionLoading ? ( + + ) : ( + + )}
Postman Collection
-
Export in Postman format
+
+ {isCollectionLoading ? 'Loading collection...' : 'Export in Postman format'} +
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 4b787fc3d..0f44b467a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -61,13 +61,14 @@ const Collection = ({ collection, searchText }) => { }; const ensureCollectionIsMounted = () => { - if (collection.mountStatus === 'unmounted') { - dispatch(mountCollection({ - collectionUid: collection.uid, - collectionPathname: collection.pathname, - brunoConfig: collection.brunoConfig - })); + if(collection.mountStatus === 'mounted'){ + return; } + dispatch(mountCollection({ + collectionUid: collection.uid, + collectionPathname: collection.pathname, + brunoConfig: collection.brunoConfig + })); } const hasSearchText = searchText && searchText?.trim()?.length; @@ -269,6 +270,7 @@ const Collection = ({ collection, searchText }) => { className="dropdown-item" onClick={(e) => { menuDropdownTippyRef.current.hide(); + ensureCollectionIsMounted(); handleRun(); }} > @@ -287,6 +289,7 @@ const Collection = ({ collection, searchText }) => { className="dropdown-item" onClick={(e) => { menuDropdownTippyRef.current.hide(); + ensureCollectionIsMounted(); setShowShareCollectionModal(true); }} > diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index 583e31725..34b3e3d5d 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -24,7 +24,7 @@ import toast from 'react-hot-toast'; import { useDispatch } from 'react-redux'; import { isElectron } from 'utils/common/platform'; import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments'; -import { collectionAddOauth2CredentialsByUrl } from 'providers/ReduxStore/slices/collections/index'; +import { collectionAddOauth2CredentialsByUrl, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index'; import { addLog } from 'providers/ReduxStore/slices/logs'; const useIpcEvents = () => { @@ -179,6 +179,10 @@ const useIpcEvents = () => { dispatch(collectionAddOauth2CredentialsByUrl(payload)); }); + const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => { + dispatch(updateCollectionLoadingState(val)); + }); + return () => { removeCollectionTreeUpdateListener(); removeOpenCollectionListener(); @@ -199,6 +203,7 @@ const useIpcEvents = () => { removeGlobalEnvironmentsUpdatesListener(); removeSnapshotHydrationListener(); removeCollectionOauth2CredentialsUpdatesListener(); + removeCollectionLoadingStateListener(); }; }, [isElectron]); }; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 44db21df4..6805fa32b 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -67,6 +67,12 @@ export const collectionsSlice = createSlice({ } } }, + updateCollectionLoadingState: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + if (collection) { + collection.isLoading = action.payload.isLoading; + } + }, setCollectionSecurityConfig: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { @@ -2413,6 +2419,7 @@ export const collectionsSlice = createSlice({ export const { createCollection, updateCollectionMountStatus, + updateCollectionLoadingState, setCollectionSecurityConfig, brunoConfigUpdateEvent, renameCollection, diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index c5ef12b04..3ea85aa61 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -137,6 +137,10 @@ export const findEnvironmentInCollectionByName = (collection, name) => { }; export const areItemsLoading = (folder) => { + if (!folder || folder.isLoading) { + return true; + } + let flattenedItems = flattenItems(folder.items); return flattenedItems?.reduce((isLoading, i) => { if (i?.loading) { diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js index 908e04ccf..e5f4503ac 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -166,7 +166,7 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => { } }; -const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread) => { +const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread, watcher) => { console.log(`watcher add: ${pathname}`); if (isBrunoConfigFile(pathname, collectionPath)) { @@ -251,6 +251,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread } if (hasBruExtension(pathname)) { + watcher.addFileToProcessing(collectionUid, pathname); + const file = { meta: { collectionUid, @@ -270,8 +272,11 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread file.size = sizeInMB(fileStats?.size); hydrateRequestWithUuid(file.data, pathname); win.webContents.send('main:collection-tree-updated', 'addFile', file); + } catch (error) { console.error(error); + } finally { + watcher.markFileAsProcessed(win, collectionUid, pathname); } return; } @@ -320,6 +325,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread file.size = sizeInMB(fileStats?.size); hydrateRequestWithUuid(file.data, pathname); win.webContents.send('main:collection-tree-updated', 'addFile', file); + } finally { + watcher.markFileAsProcessed(win, collectionUid, pathname); } } }; @@ -510,7 +517,10 @@ const unlinkDir = async (win, pathname, collectionUid, collectionPath) => { win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory); }; -const onWatcherSetupComplete = (win, watchPath) => { +const onWatcherSetupComplete = (win, watchPath, collectionUid, watcher) => { + // Mark discovery as complete + watcher.completeCollectionDiscovery(win, collectionUid); + const UiStateSnapshotStore = new UiStateSnapshot(); const collectionsSnapshotState = UiStateSnapshotStore.getCollections(); const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == watchPath); @@ -520,6 +530,75 @@ const onWatcherSetupComplete = (win, watchPath) => { class CollectionWatcher { constructor() { this.watchers = {}; + this.loadingStates = {}; + } + + // Initialize loading state tracking for a collection + initializeLoadingState(collectionUid) { + if (!this.loadingStates[collectionUid]) { + this.loadingStates[collectionUid] = { + isDiscovering: false, // Initial discovery phase + isProcessing: false, // Processing discovered files + pendingFiles: new Set(), // Files that need processing + }; + } + } + + startCollectionDiscovery(win, collectionUid) { + this.initializeLoadingState(collectionUid); + const state = this.loadingStates[collectionUid]; + + state.isDiscovering = true; + state.pendingFiles.clear(); + + win.webContents.send('main:collection-loading-state-updated', { + collectionUid, + isLoading: true + }); + } + + addFileToProcessing(collectionUid, filepath) { + this.initializeLoadingState(collectionUid); + const state = this.loadingStates[collectionUid]; + state.pendingFiles.add(filepath); + } + + markFileAsProcessed(win, collectionUid, filepath) { + if (!this.loadingStates[collectionUid]) return; + + const state = this.loadingStates[collectionUid]; + state.pendingFiles.delete(filepath); + + // If discovery is complete and no pending files, mark as not loading + if (!state.isDiscovering && state.pendingFiles.size === 0 && state.isProcessing) { + state.isProcessing = false; + win.webContents.send('main:collection-loading-state-updated', { + collectionUid, + isLoading: false + }); + } + } + + completeCollectionDiscovery(win, collectionUid) { + if (!this.loadingStates[collectionUid]) return; + + const state = this.loadingStates[collectionUid]; + state.isDiscovering = false; + + // If there are pending files, start processing phase + if (state.pendingFiles.size > 0) { + state.isProcessing = true; + } else { + // No pending files, collection is fully loaded + win.webContents.send('main:collection-loading-state-updated', { + collectionUid, + isLoading: false + }); + } + } + + cleanupLoadingState(collectionUid) { + delete this.loadingStates[collectionUid]; } addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread) { @@ -527,6 +606,10 @@ class CollectionWatcher { this.watchers[watchPath].close(); } + this.initializeLoadingState(collectionUid); + + this.startCollectionDiscovery(win, collectionUid); + const ignores = brunoConfig?.ignore || []; setTimeout(() => { const watcher = chokidar.watch(watchPath, { @@ -552,8 +635,8 @@ class CollectionWatcher { let startedNewWatcher = false; watcher - .on('ready', () => onWatcherSetupComplete(win, watchPath)) - .on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread)) + .on('ready', () => onWatcherSetupComplete(win, watchPath, collectionUid, this)) + .on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread, this)) .on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath)) .on('change', (pathname) => change(win, pathname, collectionUid, watchPath)) .on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath)) @@ -588,11 +671,15 @@ class CollectionWatcher { return this.watchers[watchPath]; } - removeWatcher(watchPath, win) { + removeWatcher(watchPath, win, collectionUid) { if (this.watchers[watchPath]) { this.watchers[watchPath].close(); this.watchers[watchPath] = null; } + + if (collectionUid) { + this.cleanupLoadingState(collectionUid); + } } getWatcherByItemPath(itemPath) { diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 089507334..8ca5ac2f9 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -593,10 +593,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } }); - ipcMain.handle('renderer:remove-collection', async (event, collectionPath) => { + ipcMain.handle('renderer:remove-collection', async (event, collectionPath, collectionUid) => { if (watcher && mainWindow) { console.log(`watcher stopWatching: ${collectionPath}`); - watcher.removeWatcher(collectionPath, mainWindow); + watcher.removeWatcher(collectionPath, mainWindow, collectionUid); lastOpenedCollections.remove(collectionPath); } }); From b571c1a1a50fd932e8c434f8a0d0b778080a2483 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <161328623+sanjaikumar-bruno@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:26:45 +0530 Subject: [PATCH 09/30] Feat/add warnings for sensitive fields other auths (#5100) --- .../EnvironmentVariables/constants.js | 11 +++ .../EnvironmentVariables/index.js | 45 ++++++++++++- .../RequestPane/Auth/AwsV4Auth/index.js | 9 ++- .../RequestPane/Auth/BasicAuth/index.js | 7 +- .../RequestPane/Auth/BearerAuth/index.js | 7 +- .../RequestPane/Auth/DigestAuth/index.js | 9 ++- .../RequestPane/Auth/NTLMAuth/index.js | 7 +- .../Auth/OAuth2/AuthorizationCode/index.js | 12 +++- .../Auth/OAuth2/ClientCredentials/index.js | 12 +++- .../Auth/OAuth2/PasswordCredentials/index.js | 12 +++- .../RequestPane/Auth/WsseAuth/index.js | 7 +- .../SensitiveFieldWarning/StyledWrapper.js | 10 +++ .../components/SensitiveFieldWarning/index.js | 29 ++++++++ .../hooks/useDetectSensitiveField/index.js | 67 +++++++++++++++++++ 14 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/constants.js create mode 100644 packages/bruno-app/src/components/SensitiveFieldWarning/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/SensitiveFieldWarning/index.js create mode 100644 packages/bruno-app/src/hooks/useDetectSensitiveField/index.js diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/constants.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/constants.js new file mode 100644 index 000000000..c7dbe0ef5 --- /dev/null +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/constants.js @@ -0,0 +1,11 @@ +const sensitiveFields = [ + 'request.auth.oauth2.clientSecret', + 'request.auth.basic.password', + 'request.auth.digest.password', + 'request.auth.wsse.password', + 'request.auth.ntlm.password', + 'request.auth.awsv4.secretAccessKey', + 'request.auth.bearer.token' +]; + +export { sensitiveFields }; diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index c777aa85f..27cab21ce 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useMemo } from 'react'; import cloneDeep from 'lodash/cloneDeep'; import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons'; import { useTheme } from 'providers/Theme'; @@ -13,7 +13,10 @@ import { variableNameRegex } from 'utils/common/regex'; import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import toast from 'react-hot-toast'; import { Tooltip } from 'react-tooltip'; -import { getGlobalEnvironmentVariables } from 'utils/collections'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { getGlobalEnvironmentVariables, flattenItems } from 'utils/collections'; +import { isItemARequest } from 'utils/collections'; +import { sensitiveFields } from './constants'; const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => { const dispatch = useDispatch(); @@ -26,6 +29,34 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); _collection.globalEnvironmentVariables = globalEnvironmentVariables; + const nonSecretSensitiveVarUsageMap = useMemo(() => { + const result = {}; + if (!collection || !environment?.variables) { + return result; + } + const nonSecretVars = environment.variables.filter((v) => v.enabled && !v.secret && v.name); + if (!nonSecretVars.length) { + return result; + } + const varNames = new Set(nonSecretVars.map((v) => v.name)); + const items = flattenItems(collection.items || []); + items.forEach((item) => { + if (!isItemARequest(item)) return; + const requestObj = item.draft ? item.draft : item; + sensitiveFields.forEach((fieldPath) => { + const value = fieldPath.split('.').reduce((obj, key) => (obj ? obj[key] : undefined), requestObj); + if (typeof value === 'string') { + varNames.forEach((varName) => { + if (new RegExp(`\{\{\s*${varName}\s*\}\}`).test(value)) { + result[varName] = true; + } + }); + } + }); + }); + return result; + }, [collection, environment]); + const formik = useFormik({ enableReinitialize: true, initialValues: environment.variables || [], @@ -61,6 +92,8 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original } }); + const hasSensitiveUsage = (name) => !!nonSecretSensitiveVarUsageMap[name]; + // Effect to track modifications. React.useEffect(() => { setIsModified(formik.dirty); @@ -163,7 +196,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
- +
formik.setFieldValue(`${index}.value`, newValue, true)} />
+ {!variable.secret && hasSensitiveUsage(variable.name) && ( + + )} { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const awsv4Auth = get(request, 'auth.awsv4', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey); const handleRun = () => dispatch(sendRequest(item, collection.uid)); @@ -144,7 +147,7 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
-
+
{ item={item} isSecret={true} /> + + {showWarning && }
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js index 752d7ce33..355b2bfd1 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -12,6 +14,8 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => { const { storedTheme } = useTheme(); const basicAuth = get(request, 'auth.basic', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(basicAuth?.password); const handleRun = () => dispatch(sendRequest(item, collection.uid)); @@ -63,7 +67,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
-
+
{ item={item} isSecret={true} /> + {showWarning && }
); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js index c8ba9d1c6..12d65cdbc 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -13,6 +15,8 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => { // Use the request prop directly like OAuth2ClientCredentials does const bearerToken = get(request, 'auth.bearer.token', ''); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(bearerToken); const handleRun = () => dispatch(sendRequest(item, collection.uid)); @@ -36,7 +40,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => { return ( -
+
{ item={item} isSecret={true} /> + {showWarning && }
); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js index a4ff3012e..d17126c37 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -11,6 +13,8 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => { const { storedTheme } = useTheme(); const digestAuth = get(request, 'auth.digest', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(digestAuth?.password); const handleRun = () => dispatch(sendRequest(item, collection.uid)); @@ -62,9 +66,9 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
-
+
handlePasswordChange(val)} @@ -73,6 +77,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => { item={item} isSecret={true} /> + {showWarning && }
); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js index 44f87656e..b1cdf474c 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -12,6 +14,8 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => { const { storedTheme } = useTheme(); const ntlmAuth = get(request, 'auth.ntlm', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password); const handleRun = () => dispatch(sendRequest(item, collection.uid)); @@ -80,7 +84,7 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
-
+
{ item={item} isSecret={true} /> + {showWarning && }
diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js index c00964d82..c46ce951d 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js @@ -1,4 +1,5 @@ import React, { useRef, forwardRef } from 'react'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -9,13 +10,14 @@ import StyledWrapper from './StyledWrapper'; import { inputsConfig } from './inputsConfig'; import Oauth2TokenViewer from '../Oauth2TokenViewer/index'; import Oauth2ActionButtons from '../Oauth2ActionButtons/index'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - + const { isSensitive } = useDetectSensitiveField(collection); const oAuth = get(request, 'auth.oauth2', {}); const { callbackUrl, @@ -129,12 +131,15 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu
{inputsConfig.map((input) => { const { key, label, isSecret } = input; + const value = oAuth[key] || ''; + const { showWarning, warningMessage } = isSensitive(value); + return (
-
+
handleChange(key, val)} @@ -143,6 +148,7 @@ const OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAu item={item} isSecret={isSecret} /> + {isSecret && showWarning && }
); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js index 64ab7c408..667e965a6 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js @@ -1,4 +1,5 @@ import React, { useRef, forwardRef } from 'react'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -9,13 +10,14 @@ import { inputsConfig } from './inputsConfig'; import Dropdown from 'components/Dropdown'; import Oauth2TokenViewer from '../Oauth2TokenViewer/index'; import Oauth2ActionButtons from '../Oauth2ActionButtons/index'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - + const { isSensitive } = useDetectSensitiveField(collection); const oAuth = get(request, 'auth.oauth2', {}); const { @@ -96,12 +98,15 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
{inputsConfig.map((input) => { const { key, label, isSecret } = input; + const value = oAuth[key] || ''; + const { showWarning, warningMessage } = isSensitive(value); + return (
-
+
handleChange(key, val)} @@ -110,6 +115,7 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu item={item} isSecret={isSecret} /> + {isSecret && showWarning && }
); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js index 385607848..db969d8cc 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js @@ -1,4 +1,5 @@ import React, { useRef, forwardRef } from 'react'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -9,14 +10,15 @@ import { inputsConfig } from './inputsConfig'; import Dropdown from 'components/Dropdown'; import Oauth2TokenViewer from '../Oauth2TokenViewer/index'; import Oauth2ActionButtons from '../Oauth2ActionButtons/index'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index'; const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - const oAuth = get(request, 'auth.oauth2', {}); + const { isSensitive } = useDetectSensitiveField(collection); const { accessTokenUrl, @@ -99,12 +101,15 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
{inputsConfig.map((input) => { const { key, label, isSecret } = input; + const value = oAuth[key] || ''; + const { showWarning, warningMessage } = isSensitive(value); + return (
-
+
handleChange(key, val)} @@ -113,6 +118,7 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update item={item} isSecret={isSecret} /> + {isSecret && showWarning && }
); diff --git a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js index 05e9daaf1..fde2310c9 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -12,6 +14,8 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => { const { storedTheme } = useTheme(); const wsseAuth = get(request, 'auth.wsse', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(wsseAuth?.password); const handleRun = () => dispatch(sendRequest(item, collection.uid)); @@ -63,7 +67,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
-
+
{ item={item} isSecret={true} /> + {showWarning && }
); diff --git a/packages/bruno-app/src/components/SensitiveFieldWarning/StyledWrapper.js b/packages/bruno-app/src/components/SensitiveFieldWarning/StyledWrapper.js new file mode 100644 index 000000000..97a9389d8 --- /dev/null +++ b/packages/bruno-app/src/components/SensitiveFieldWarning/StyledWrapper.js @@ -0,0 +1,10 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + .tooltip-mod { + font-size: 11px !important; + width: 150px !important; + } +`; + +export default Wrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/SensitiveFieldWarning/index.js b/packages/bruno-app/src/components/SensitiveFieldWarning/index.js new file mode 100644 index 000000000..2b2cce326 --- /dev/null +++ b/packages/bruno-app/src/components/SensitiveFieldWarning/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { IconAlertTriangle } from '@tabler/icons'; +import { Tooltip } from 'react-tooltip'; +import StyledWrapper from './StyledWrapper'; + +const SensitiveFieldWarning = ({ fieldName, warningMessage }) => { + const tooltipId = `sensitive-field-warning-${fieldName}`; + + return ( + + + + +

+ {warningMessage} +

+
+ } + /> + +
+ ); +}; + +export default SensitiveFieldWarning; diff --git a/packages/bruno-app/src/hooks/useDetectSensitiveField/index.js b/packages/bruno-app/src/hooks/useDetectSensitiveField/index.js new file mode 100644 index 000000000..257b116e9 --- /dev/null +++ b/packages/bruno-app/src/hooks/useDetectSensitiveField/index.js @@ -0,0 +1,67 @@ +import { useMemo } from 'react'; + +const VARIABLE_NAME_REGEX = /\{\{([^}]+)\}\}/g; +const ENV_VAR_REFERENCE_REGEX = /^\s*\{\{.*\}\}\s*$/; + +export const useDetectSensitiveField = (collection) => { + const envVars = useMemo(() => { + if (!collection) { + return []; + } + const activeEnv = collection?.environments?.find((env) => env.uid === collection.activeEnvironmentUid); + if (!activeEnv || !Array.isArray(activeEnv.variables)) { + return []; + } + return activeEnv.variables; + }, [collection]); + + // Checks if the value is a single environment variable reference (e.g., {{API_KEY}}) + const isEnvVarReference = (value) => { + return typeof value === 'string' && ENV_VAR_REFERENCE_REGEX.test(value); + }; + + // Extracts all variable names from a string (e.g., "Bearer {{TOKEN}}-{{SUFFIX}}" → ["TOKEN", "SUFFIX"]) + const extractVarNames = (value) => { + if (!value || typeof value !== 'string') { + return []; + } + const matches = []; + let match; + while ((match = VARIABLE_NAME_REGEX.exec(value)) !== null) { + matches.push(match[1].trim()); + } + return matches; + }; + + // Checks if a variable is present and not marked as secret in the environment + const isVarNotSecret = (varName, envVars = []) => { + const found = envVars.find((v) => v.name === varName); + return found && !found.secret; + }; + + const isSensitive = (value) => { + if (value && !isEnvVarReference(value)) { + return { + showWarning: true, + warningMessage: 'Store sensitive info as a secret variable or in a .env file' + }; + } + + if (value && typeof value === 'string') { + const varNames = extractVarNames(value); + if (varNames.some((varName) => isVarNotSecret(varName, envVars))) { + return { + showWarning: true, + warningMessage: 'Mark the environment variable as secret for better security.' + }; + } + } + + // No warning needed + return { showWarning: false }; + }; + + return { + isSensitive + }; +}; From 5150251698ebb34dfd2cf9985f40c8afa9cb691a Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Wed, 30 Jul 2025 00:27:24 +0530 Subject: [PATCH 10/30] fix: params while involking renderer:remove-collection (#5218) --- .../src/providers/ReduxStore/slices/collections/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 2df8ee03b..20e9e093f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -1064,7 +1064,7 @@ export const removeCollection = (collectionUid) => (dispatch, getState) => { } const { ipcRenderer } = window; ipcRenderer - .invoke('renderer:remove-collection', collection.pathname) + .invoke('renderer:remove-collection', collection.pathname, collectionUid) .then(() => { dispatch(closeAllCollectionTabs({ collectionUid })); }) From aae4f03fdf3c2f7448a91a9c3a17113e6d274bd5 Mon Sep 17 00:00:00 2001 From: maintainer-bruno Date: Wed, 30 Jul 2025 18:45:28 +0530 Subject: [PATCH 11/30] fix(dev): add filestore src to dev hot reload watchers (#5223) --- package.json | 1 + scripts/dev-hot-reload.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/package.json b/package.json index 782d89866..a71957ed1 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "setup": "node ./scripts/setup.js", "watch:converters": "npm run watch --workspace=packages/bruno-converters", "dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"", + "watch": "npm run dev:watch", "dev:watch": "node ./scripts/dev-hot-reload.js", "dev:web": "npm run dev --workspace=packages/bruno-app", "build:web": "npm run build --workspace=packages/bruno-app", diff --git a/scripts/dev-hot-reload.js b/scripts/dev-hot-reload.js index 52440f279..7c059a8f5 100644 --- a/scripts/dev-hot-reload.js +++ b/scripts/dev-hot-reload.js @@ -170,6 +170,11 @@ function startDevelopment() { name: 'requests', prefixColor: 'gray' }, + { + command: 'npm run watch --workspace=packages/bruno-filestore', + name: 'filestore', + prefixColor: '#FA8072' + }, { command: 'npm run dev:web', name: 'react', From 60a0a32743e3e5ffe55fbb260b7a64c253dd5e84 Mon Sep 17 00:00:00 2001 From: Tim Nikischin <49103409+nikischin@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:05:17 +0200 Subject: [PATCH 12/30] Implement Response URL variable (#2983) --- .../src/runner/run-single-request.js | 3 +++ .../bruno-electron/src/ipc/network/index.js | 2 ++ packages/bruno-js/src/bruno-response.js | 5 ++++ .../sandbox/quickjs/shims/bruno-response.js | 9 ++++++++ packages/bruno-js/src/utils.js | 1 + .../collection/scripting/api/res/getUrl.bru | 23 +++++++++++++++++++ 6 files changed, 43 insertions(+) create mode 100644 packages/bruno-tests/collection/scripting/api/res/getUrl.bru diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 55397c7c4..ed1e554e0 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -463,6 +463,7 @@ const runSingleRequest = async function ( statusText: null, headers: null, data: null, + url: null, responseTime: 0 }, error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!', @@ -602,6 +603,7 @@ const runSingleRequest = async function ( statusText: response.statusText, headers: response.headers, data: response.data, + url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null, responseTime }, error: null, @@ -630,6 +632,7 @@ const runSingleRequest = async function ( statusText: null, headers: null, data: null, + url: null, responseTime: 0 }, status: 'error', diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 078cc20c4..8e39e0d5c 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -901,6 +901,7 @@ const registerNetworkIpc = (mainWindow) => { dataBuffer: response.dataBuffer.toString('base64'), size: Buffer.byteLength(response.dataBuffer), duration: responseTime ?? 0, + url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null, timeline: response.timeline }; } catch (error) { @@ -1208,6 +1209,7 @@ const registerNetworkIpc = (mainWindow) => { data: response.data, responseTime: response.responseTime, timeline: response.timeline, + url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null }, ...eventData }); diff --git a/packages/bruno-js/src/bruno-response.js b/packages/bruno-js/src/bruno-response.js index 1705d606f..2a482c5e7 100644 --- a/packages/bruno-js/src/bruno-response.js +++ b/packages/bruno-js/src/bruno-response.js @@ -9,6 +9,7 @@ class BrunoResponse { this.headers = res ? res.headers : null; this.body = res ? res.data : null; this.responseTime = res ? res.responseTime : null; + this.url = res?.request ? res.request.protocol + '//' + res.request.host + res.request.path : null; // Make the instance callable const callable = (...args) => get(this.body, ...args); @@ -42,6 +43,10 @@ class BrunoResponse { return this.res ? this.res.responseTime : null; } + getUrl() { + return this.res ? this.url : null; + } + setBody(data) { if (!this.res) { return; diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js index 50aca92ac..df0fabe60 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js @@ -10,17 +10,20 @@ const addBrunoResponseShimToContext = (vm, res) => { const headers = marshallToVm(res?.headers, vm); const body = marshallToVm(res?.body, vm); const responseTime = marshallToVm(res?.responseTime, vm); + const url = marshallToVm(res?.url, vm); vm.setProp(resFn, 'status', status); vm.setProp(resFn, 'statusText', statusText); vm.setProp(resFn, 'headers', headers); vm.setProp(resFn, 'body', body); vm.setProp(resFn, 'responseTime', responseTime); + vm.setProp(resFn, 'url', url); status.dispose(); headers.dispose(); body.dispose(); responseTime.dispose(); + url.dispose(); statusText.dispose(); let getStatusText = vm.newFunction('getStatusText', function () { @@ -59,6 +62,12 @@ const addBrunoResponseShimToContext = (vm, res) => { vm.setProp(resFn, 'getResponseTime', getResponseTime); getResponseTime.dispose(); + let getUrl = vm.newFunction('getUrl', function () { + return marshallToVm(res.getUrl(), vm); + }); + vm.setProp(resFn, 'getUrl', getUrl); + getUrl.dispose(); + let setBody = vm.newFunction('setBody', function (data) { res.setBody(vm.dump(data)); }); diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index 7ebfa795a..ca26d6f4d 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -117,6 +117,7 @@ const createResponseParser = (response = {}) => { res.headers = response.headers; res.body = response.data; res.responseTime = response.responseTime; + res.url = response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null; res.jq = (expr) => { const output = jsonQuery(expr, { data: response.data }); diff --git a/packages/bruno-tests/collection/scripting/api/res/getUrl.bru b/packages/bruno-tests/collection/scripting/api/res/getUrl.bru new file mode 100644 index 000000000..fc85436c5 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/res/getUrl.bru @@ -0,0 +1,23 @@ +meta { + name: getUrl + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +assert { + res.status: eq 200 + res.body: eq pong +} + +tests { + test("res.getUrl()", function() { + const url = res.getUrl(); + expect(url).to.equal("https://testbench-sanity.usebruno.com/ping"); + }); +} From 31027cb2e07b8ec6c592b5c84cbe7d7e25b1b5a6 Mon Sep 17 00:00:00 2001 From: Pooja Date: Wed, 30 Jul 2025 19:35:54 +0530 Subject: [PATCH 13/30] feat: adding cookie apis (#5117) --- package-lock.json | 138 ++++- .../src/utils/codemirror/autocomplete.js | 12 +- packages/bruno-cli/package.json | 9 +- .../src/runner/run-single-request.js | 2 +- packages/bruno-cli/src/utils/cookies.js | 104 +--- packages/bruno-common/jest.config.js | 2 +- packages/bruno-common/package.json | 1 + packages/bruno-common/src/cookies/index.ts | 500 ++++++++++++++++++ packages/bruno-common/src/index.ts | 1 + packages/bruno-common/src/utils/index.ts | 6 +- .../src/utils/url/validation.spec.ts} | 4 +- .../bruno-common/src/utils/url/validation.ts | 67 +++ .../tests/cookies/cookie-jar-wrapper.spec.js | 228 ++++++++ .../src/postman/postman-translations.js | 7 + .../src/utils/jscode-shift-translator.js | 68 +++ .../postman-comments.spec.js | 4 +- .../postman-cookie-conversions.spec.js | 319 +++++++++++ packages/bruno-electron/package.json | 5 +- .../bruno-electron/src/ipc/network/index.js | 18 + packages/bruno-electron/src/utils/cookies.js | 198 +------ packages/bruno-js/src/bru.js | 45 ++ .../bruno-js/src/sandbox/quickjs/shims/bru.js | 166 +++++- packages/bruno-requests/src/index.ts | 2 - .../bruno-requests/src/utils/cookie-utils.js | 105 ---- packages/bruno-requests/src/utils/index.ts | 1 - .../scripting/api/bru/cookies/clear.bru | 67 +++ .../api/bru/cookies/deleteCookie.bru | 75 +++ .../api/bru/cookies/deleteCookies.bru | 106 ++++ .../scripting/api/bru/cookies/folder.bru | 8 + .../scripting/api/bru/cookies/getCookie.bru | 38 ++ .../scripting/api/bru/cookies/getCookies.bru | 52 ++ .../scripting/api/bru/cookies/setCookie.bru | 69 +++ .../api/bru/cookies/setCookieHeader.bru | 40 ++ .../scripting/api/bru/cookies/setCookies.bru | 85 +++ 34 files changed, 2125 insertions(+), 427 deletions(-) create mode 100644 packages/bruno-common/src/cookies/index.ts rename packages/{bruno-requests/src/utils/cookie-utils.spec.js => bruno-common/src/utils/url/validation.spec.ts} (98%) create mode 100644 packages/bruno-common/src/utils/url/validation.ts create mode 100644 packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js create mode 100644 packages/bruno-converters/tests/postman/postman-translations/postman-cookie-conversions.spec.js delete mode 100644 packages/bruno-requests/src/utils/cookie-utils.js delete mode 100644 packages/bruno-requests/src/utils/index.ts create mode 100644 packages/bruno-tests/collection/scripting/api/bru/cookies/clear.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookie.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookies.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/cookies/folder.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/cookies/getCookie.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/cookies/getCookies.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/cookies/setCookie.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/cookies/setCookieHeader.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/cookies/setCookies.bru diff --git a/package-lock.json b/package-lock.json index f3c68d89c..0ee290ea7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11521,6 +11521,33 @@ "node": ">=6" } }, + "node_modules/clone-regexp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz", + "integrity": "sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==", + "dev": true, + "dependencies": { + "is-regexp": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clone-regexp/node_modules/is-regexp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", + "integrity": "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", @@ -11953,6 +11980,18 @@ "node": ">= 0.6" } }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -15557,6 +15596,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-0.1.1.tgz", + "integrity": "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -16902,6 +16953,14 @@ "node": ">= 12" } }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -17155,6 +17214,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -26277,6 +26347,23 @@ "dev": true, "license": "MIT" }, + "node_modules/super-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-0.2.0.tgz", + "integrity": "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==", + "dev": true, + "dependencies": { + "clone-regexp": "^3.0.0", + "function-timeout": "^0.1.0", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -26809,6 +26896,21 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "dev": true, + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/timers-browserify": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", @@ -26906,6 +27008,7 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.33", @@ -26921,6 +27024,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -29943,7 +30047,6 @@ "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", - "tough-cookie": "^4.1.3", "xmlbuilder": "^15.1.1", "yargs": "^17.6.2" }, @@ -31007,6 +31110,9 @@ "name": "@usebruno/common", "version": "0.1.0", "license": "MIT", + "dependencies": { + "is-ip": "^3.1.0" + }, "devDependencies": { "@babel/preset-env": "^7.26.9", "@babel/preset-typescript": "^7.27.0", @@ -31017,6 +31123,7 @@ "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^29.5.14", "babel-jest": "^29.7.0", + "is-ip": "^5.0.1", "moment": "^2.29.4", "rollup": "3.29.5", "rollup-plugin-dts": "^5.0.0", @@ -31537,6 +31644,34 @@ "node": ">=4" } }, + "packages/bruno-common/node_modules/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/bruno-common/node_modules/is-ip": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-5.0.1.tgz", + "integrity": "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==", + "dev": true, + "dependencies": { + "ip-regex": "^5.0.0", + "super-regex": "^0.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/bruno-common/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -31723,7 +31858,6 @@ "nanoid": "3.3.8", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", - "tough-cookie": "^4.1.3", "uuid": "^9.0.0", "yup": "^0.32.11" }, diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js index 7b65f4234..adfa74939 100644 --- a/packages/bruno-app/src/utils/codemirror/autocomplete.js +++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js @@ -78,7 +78,17 @@ const STATIC_API_HINTS = { 'bru.runner.setNextRequest(requestName)', 'bru.runner.skipRequest()', 'bru.runner.stopExecution()', - 'bru.interpolate(str)' + 'bru.interpolate(str)', + 'bru.cookies', + 'bru.cookies.jar()', + 'bru.cookies.jar().getCookie(url, name, callback)', + 'bru.cookies.jar().getCookies(url, callback)', + 'bru.cookies.jar().setCookie(url, name, value, callback)', + 'bru.cookies.jar().setCookie(url, cookieObject, callback)', + 'bru.cookies.jar().setCookies(url, cookiesArray, callback)', + 'bru.cookies.jar().clear(callback)', + 'bru.cookies.jar().deleteCookies(url, callback)', + 'bru.cookies.jar().deleteCookie(url, name, callback)', ] }; diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index ca55861f1..c298273b4 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -48,12 +48,12 @@ "dependencies": { "@aws-sdk/credential-providers": "3.750.0", "@usebruno/common": "0.1.0", - "@usebruno/js": "0.12.0", - "@usebruno/lang": "0.12.0", - "@usebruno/vm2": "^3.9.13", - "@usebruno/requests": "^0.1.0", "@usebruno/converters": "^0.1.0", "@usebruno/filestore": "^0.1.0", + "@usebruno/js": "0.12.0", + "@usebruno/lang": "0.12.0", + "@usebruno/requests": "^0.1.0", + "@usebruno/vm2": "^3.9.13", "aws4-axios": "^3.3.0", "axios": "^1.8.3", "axios-ntlm": "^1.4.2", @@ -69,7 +69,6 @@ "lodash": "^4.17.21", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", - "tough-cookie": "^4.1.3", "xmlbuilder": "^15.1.1", "yargs": "^17.6.2" } diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index ed1e554e0..a682d4814 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -20,7 +20,7 @@ const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-he const { shouldUseProxy, PatchedHttpsProxyAgent, getSystemProxyEnvVariables } = require('../utils/proxy-util'); const path = require('path'); const { parseDataFromResponse } = require('../utils/common'); -const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies'); +const { getCookieStringForUrl, saveCookies } = require('../utils/cookies'); const { createFormData } = require('../utils/form-data'); const { getOAuth2Token } = require('./oauth2'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; diff --git a/packages/bruno-cli/src/utils/cookies.js b/packages/bruno-cli/src/utils/cookies.js index 01a82316b..f4aaef547 100644 --- a/packages/bruno-cli/src/utils/cookies.js +++ b/packages/bruno-cli/src/utils/cookies.js @@ -1,103 +1 @@ -const { Cookie, CookieJar } = require('tough-cookie'); -const each = require('lodash/each'); -const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils; - -const cookieJar = new CookieJar(); - -const addCookieToJar = (setCookieHeader, requestUrl) => { - const cookie = Cookie.parse(setCookieHeader, { loose: true }); - cookieJar.setCookieSync(cookie, requestUrl, { - ignoreError: true // silently ignore things like parse errors and invalid domains - }); -}; - -const getCookiesForUrl = (url) => { - return cookieJar.getCookiesSync(url, { - secure: isPotentiallyTrustworthyOrigin(url) - }); -}; - -const getCookieStringForUrl = (url) => { - const cookies = getCookiesForUrl(url); - - if (!Array.isArray(cookies) || !cookies.length) { - return ''; - } - - const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); - - return validCookies.map((cookie) => cookie.cookieString()).join('; '); -}; - -const getDomainsWithCookies = () => { - return new Promise((resolve, reject) => { - const domainCookieMap = {}; - - cookieJar.store.getAllCookies((err, cookies) => { - if (err) { - return reject(err); - } - - cookies.forEach((cookie) => { - if (!domainCookieMap[cookie.domain]) { - domainCookieMap[cookie.domain] = [cookie]; - } else { - domainCookieMap[cookie.domain].push(cookie); - } - }); - - const domains = Object.keys(domainCookieMap); - const domainsWithCookies = []; - - each(domains, (domain) => { - const cookies = domainCookieMap[domain]; - const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); - - if (validCookies.length) { - domainsWithCookies.push({ - domain, - cookies: validCookies, - cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ') - }); - } - }); - - resolve(domainsWithCookies); - }); - }); -}; - -const deleteCookiesForDomain = (domain) => { - return new Promise((resolve, reject) => { - cookieJar.store.removeCookies(domain, null, (err) => { - if (err) { - return reject(err); - } - - return resolve(); - }); - }); -}; - -const saveCookies = (url, headers) => { - let setCookieHeaders = []; - if (headers['set-cookie']) { - setCookieHeaders = Array.isArray(headers['set-cookie']) - ? headers['set-cookie'] - : [headers['set-cookie']]; - for (let setCookieHeader of setCookieHeaders) { - if (typeof setCookieHeader === 'string' && setCookieHeader.length) { - addCookieToJar(setCookieHeader, url); - } - } - } -} - -module.exports = { - addCookieToJar, - getCookiesForUrl, - getCookieStringForUrl, - getDomainsWithCookies, - deleteCookiesForDomain, - saveCookies -}; +module.exports = require('@usebruno/common').cookies; diff --git a/packages/bruno-common/jest.config.js b/packages/bruno-common/jest.config.js index cd4a5f5ae..546abd629 100644 --- a/packages/bruno-common/jest.config.js +++ b/packages/bruno-common/jest.config.js @@ -3,7 +3,7 @@ module.exports = { '^.+\\.(ts|js)$': 'babel-jest', }, transformIgnorePatterns: [ - '/node_modules/(?!(lodash-es)/)', + '/node_modules/(?!(lodash-es|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp)/)' ], testEnvironment: 'node' }; diff --git a/packages/bruno-common/package.json b/packages/bruno-common/package.json index 66ded2519..c989069df 100644 --- a/packages/bruno-common/package.json +++ b/packages/bruno-common/package.json @@ -46,6 +46,7 @@ "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^29.5.14", "babel-jest": "^29.7.0", + "is-ip": "^5.0.1", "moment": "^2.29.4", "rollup": "3.29.5", "rollup-plugin-dts": "^5.0.0", diff --git a/packages/bruno-common/src/cookies/index.ts b/packages/bruno-common/src/cookies/index.ts new file mode 100644 index 000000000..2bfbf7a77 --- /dev/null +++ b/packages/bruno-common/src/cookies/index.ts @@ -0,0 +1,500 @@ +import { Cookie, CookieJar } from 'tough-cookie'; +import each from 'lodash/each'; +import moment from 'moment'; +import { isPotentiallyTrustworthyOrigin } from '../utils'; + +const cookieJar = new CookieJar(); + +const addCookieToJar = (setCookieHeader: string, requestUrl: string): void => { + const cookie = Cookie.parse(setCookieHeader, { loose: true }); + if (!cookie) return; + cookieJar.setCookieSync(cookie, requestUrl, { + ignoreError: true + }); +}; + +const getCookiesForUrl = (url: string) => { + return cookieJar.getCookiesSync(url, { + secure: isPotentiallyTrustworthyOrigin(url) + }); +}; + +const getCookieStringForUrl = (url: string): string => { + const cookies = getCookiesForUrl(url); + if (!Array.isArray(cookies) || !cookies.length) return ''; + + const validCookies = cookies.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now()); + return validCookies.map((cookie) => cookie.cookieString()).join('; '); +}; + +const getDomainsWithCookies = (): Promise> => { + return new Promise((resolve, reject) => { + const domainCookieMap: Record = {}; + + (cookieJar as any).store.getAllCookies((err: Error, cookies: Cookie[]) => { + if (err) return reject(err); + + cookies.forEach((cookie) => { + // Handle null domain by skipping the cookie + if (!cookie.domain) return; + + if (!domainCookieMap[cookie.domain]) { + domainCookieMap[cookie.domain] = [cookie]; + } else { + domainCookieMap[cookie.domain].push(cookie); + } + }); + + const domains = Object.keys(domainCookieMap); + const domainsWithCookies: Array<{ domain: string; cookies: Cookie[]; cookieString: string }> = []; + + each(domains, (domain) => { + const cookiesForDomain = domainCookieMap[domain]; + const validCookies = cookiesForDomain.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now()); + + if (validCookies.length) { + domainsWithCookies.push({ + domain, + cookies: validCookies, + cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ') + }); + } + }); + + resolve(domainsWithCookies); + }); + }); +}; + +const deleteCookie = (domain: string, path: string, cookieKey: string): Promise => { + return new Promise((resolve, reject) => { + (cookieJar as any).store.removeCookie(domain, path, cookieKey, (err: Error) => { + if (err) return reject(err); + resolve(); + }); + }); +}; + +const deleteCookiesForDomain = (domain: string): Promise => { + return new Promise((resolve, reject) => { + (cookieJar as any).store.removeCookies(domain, null, (err: Error) => { + if (err) return reject(err); + resolve(); + }); + }); +}; + +const updateCookieObj = (cookieObj: any, oldCookie: Cookie) => { + return { + ...cookieObj, + path: oldCookie.path, + key: oldCookie.key, + domain: oldCookie.domain, + expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity, + creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(), + lastAccessed: + oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid() + ? new Date(oldCookie.lastAccessed) + : new Date() + } as any; +}; + +const createCookieObj = (cookieObj: any) => { + return { + ...cookieObj, + path: cookieObj.path, + expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity, + creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(), + lastAccessed: + cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid() + ? new Date(cookieObj.lastAccessed) + : new Date() + } as any; +}; + +const addCookieForDomain = (domain: string, cookieObj: any): Promise => { + return new Promise((resolve, reject) => { + try { + const cookie = new Cookie(createCookieObj(cookieObj)); + (cookieJar as any).store.putCookie(cookie, (err: Error) => { + if (err) return reject(err); + resolve(); + }); + } catch (err) { + reject(err); + } + }); +}; + +const modifyCookieForDomain = (domain: string, oldCookieObj: any, cookieObj: any): Promise => { + return new Promise((resolve, reject) => { + try { + const oldCookie = new Cookie(createCookieObj(oldCookieObj)); + const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie)); + (cookieJar as any).store.updateCookie(oldCookie, newCookie, (removeErr: Error) => { + if (removeErr) return reject(removeErr); + resolve(); + }); + } catch (err) { + reject(err); + } + }); +}; + +const parseCookieString = (cookieStr: string): any | null => { + try { + const cookie = Cookie.parse(cookieStr); + if (!cookie) return null; + return { + ...cookie, + expires: cookie.expires === 'Infinity' || (cookie.expires as any) === Infinity ? null : cookie.expires + }; + } catch (err) { + throw err; + } +}; + +const createCookieString = (cookieObj: any): string => { + const cookie = new Cookie(createCookieObj(cookieObj)); + let cookieString = cookie.toString(); // tough-cookie omits domain + + // Manually append domain if cookie is hostOnly but we still want Domain flag + if (cookieObj.hostOnly && !cookieString.includes('Domain=')) { + cookieString += `; Domain=${cookieObj.domain}`; + } + return cookieString; +} + +const saveCookies = (url: string, headers: any) => { + if (headers['set-cookie']) { + let setCookieHeaders = Array.isArray(headers['set-cookie']) + ? headers['set-cookie'] + : [headers['set-cookie']]; + for (let setCookieHeader of setCookieHeaders) { + if (typeof setCookieHeader === 'string' && setCookieHeader.length) { + addCookieToJar(setCookieHeader, url); + } + } + } +}; + +const cookieJarWrapper = () => { + return { + + // Get the full cookie object for the given URL & name. + getCookie: function ( + url: string, + cookieName: string, + callback?: (err: Error | null | undefined, cookie?: Cookie | null) => void + ) { + if (!url || !cookieName) { + const error = new Error('URL and cookie name are required'); + if (callback) return callback(error); + return Promise.reject(error); + } + + if (callback) { + // Callback mode + return cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return callback(err); + const cookie = cookies.find((c) => c.key === cookieName); + callback(null, cookie || null); + }); + } + + // Promise mode + return new Promise((resolve, reject) => { + cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return reject(err); + const cookie = cookies.find((c) => c.key === cookieName); + resolve(cookie || null); + }); + }); + }, + + // Get all cookies that would be sent to the given URL. + getCookies: function (url: string, callback?: (err: Error | null | undefined, cookies?: Cookie[]) => void) { + if (!url) { + const error = new Error('URL is required'); + if (callback) return callback(error); + return Promise.reject(error); + } + + if (callback) { + // Callback mode + return cookieJar.getCookies(url, callback); + } + + // Promise mode + return new Promise((resolve, reject) => { + cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return reject(err); + resolve(cookies); + }); + }); + }, + + setCookie: function ( + url: string, + nameOrCookieObj: string | Record, + valueOrCallback?: string | ((err?: Error | undefined) => void), + maybeCallback?: (err?: Error | undefined) => void + ) { + // Determine the callback + let callback: ((err?: Error | undefined) => void) | undefined; + if (typeof maybeCallback === 'function') { + callback = maybeCallback; + } else if (typeof valueOrCallback === 'function') { + callback = valueOrCallback as (err?: Error | undefined) => void; + } + + const executeSetCookie = () => { + if (!url) throw new Error('URL is required'); + + // CASE 1: name/value pair provided + if (typeof nameOrCookieObj === 'string') { + const cookieName = nameOrCookieObj; + const cookieValue = typeof valueOrCallback === 'string' ? valueOrCallback : ''; + + if (!cookieName) throw new Error('Cookie name is required'); + + const cookie = new Cookie({ + key: cookieName, + value: cookieValue, + domain: new URL(url).hostname, + }); + + cookieJar.setCookieSync(cookie, url, { ignoreError: true }); + return; + } + + // CASE 2: cookie object provided + if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) { + const obj = { ...(nameOrCookieObj as any) } as any; + + if (!obj.key && obj.name) obj.key = obj.name; + if (!obj.key) throw new Error('cookieObject.key (name) is required'); + + const base = { + domain: new URL(url).hostname, + ...obj, + } as any; + + const processedCookie = createCookieObj(base); + const cookie = new Cookie(processedCookie); + cookieJar.setCookieSync(cookie, url, { ignoreError: true }); + return; + } + + // If we reach here, arguments were invalid + throw new Error('Invalid arguments passed to setCookie'); + }; + + if (callback) { + // Callback mode + try { + executeSetCookie(); + callback(undefined); + } catch (err) { + callback(err as Error); + } + return; + } + + // Promise mode + return new Promise((resolve, reject) => { + try { + executeSetCookie(); + resolve(); + } catch (err) { + reject(err); + } + }); + }, + + + setCookies: function ( + url: string, + cookiesArray: any[], + callback?: (err?: Error | undefined) => void + ) { + const executeSetCookies = () => { + if (!url) throw new Error('URL is required'); + if (!Array.isArray(cookiesArray)) { + throw new Error('setCookies expects an array of cookie objects'); + } + + for (const cookieObject of cookiesArray) { + const obj = { ...(cookieObject as any) } as any; + + if (!obj.key && obj.name) obj.key = obj.name; + if (!obj.key) throw new Error('cookieObject.key (name) is required'); + + const base = { + domain: new URL(url).hostname, + ...obj + } as any; + + const processedCookie = createCookieObj(base); + const cookie = new Cookie(processedCookie); + cookieJar.setCookieSync(cookie, url, { ignoreError: true }); + } + }; + + if (callback) { + // Callback mode + try { + executeSetCookies(); + callback(undefined); + } catch (err) { + callback(err as Error); + } + return; + } + + // Promise mode + return new Promise((resolve, reject) => { + try { + executeSetCookies(); + resolve(); + } catch (err) { + reject(err); + } + }); + }, + + + clear: function (callback?: (err?: Error | undefined) => void) { + if (callback) { + // Callback mode + return (cookieJar as any).store.removeAllCookies(callback); + } + + // Promise mode + return new Promise((resolve, reject) => { + (cookieJar as any).store.removeAllCookies((err?: Error) => { + if (err) reject(err); + else resolve(); + }); + }); + }, + + deleteCookies: function (url: string, callback?: (err?: Error | undefined) => void) { + if (!url) { + const error = new Error('URL is required'); + if (callback) return callback(error); + return Promise.reject(error); + } + + if (callback) { + // Callback mode + return cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return callback(err); + if (!cookies || !cookies.length) return callback(undefined); + + let pending = cookies.length; + const done = (removeErr?: Error) => { + if (removeErr) return callback(removeErr); + if (--pending === 0) { + callback(undefined); + } + }; + + cookies.forEach((cookie) => { + (cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done); + }); + }); + } + + // Promise mode + return new Promise((resolve, reject) => { + cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return reject(err); + if (!cookies || !cookies.length) return resolve(); + + let pending = cookies.length; + const done = (removeErr?: Error) => { + if (removeErr) return reject(removeErr); + if (--pending === 0) { + resolve(); + } + }; + + cookies.forEach((cookie) => { + (cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done); + }); + }); + }); + }, + + deleteCookie: function (url: string, cookieName: string, callback?: (err?: Error | undefined) => void) { + if (!url || !cookieName) { + const error = new Error('URL and cookie name are required'); + if (callback) return callback(error); + return Promise.reject(error); + } + + const executeDelete = (callback: (err?: Error) => void) => { + cookieJar.getCookies(url, (err: Error | null, cookies: Cookie[]) => { + if (err) return callback(err); + + // Filter cookies matching key + const matchingCookies = (cookies || []).filter((c) => c.key === cookieName); + if (!matchingCookies.length) return callback(undefined); + + const urlPath = new URL(url).pathname || '/'; + + // Prioritise a cookie whose path exactly matches the URL path + let cookieToDelete = matchingCookies.find((c) => c.path === urlPath); + + // If not found, fall back to the first matching cookie (most specific path first) + if (!cookieToDelete) { + // tough-cookie sorts cookies by path length desc, preserve that order + cookieToDelete = matchingCookies[0]; + } + + (cookieJar as any).store.removeCookie( + cookieToDelete.domain, + cookieToDelete.path, + cookieToDelete.key, + callback + ); + }); + }; + + if (callback) { + // Callback mode + return executeDelete(callback); + } + + // Promise mode + return new Promise((resolve, reject) => { + executeDelete((err?: Error) => { + if (err) reject(err); + else resolve(); + }); + }); + } + } as const; +}; + + +const cookiesModule = { + cookieJar, + addCookieToJar, + getCookiesForUrl, + getCookieStringForUrl, + getDomainsWithCookies, + deleteCookie, + deleteCookiesForDomain, + addCookieForDomain, + modifyCookieForDomain, + parseCookieString, + createCookieString, + updateCookieObj, + createCookieObj, + jar: cookieJarWrapper, + saveCookies +}; + +export default cookiesModule; \ No newline at end of file diff --git a/packages/bruno-common/src/index.ts b/packages/bruno-common/src/index.ts index e72c1d847..15e55346e 100644 --- a/packages/bruno-common/src/index.ts +++ b/packages/bruno-common/src/index.ts @@ -1,5 +1,6 @@ export { mockDataFunctions } from './utils/faker-functions'; export { default as interpolate } from './interpolate'; export { default as isRequestTagsIncluded } from './tags'; +export { default as cookies } from './cookies'; export * as utils from './utils'; \ No newline at end of file diff --git a/packages/bruno-common/src/utils/index.ts b/packages/bruno-common/src/utils/index.ts index 70c92ea7c..4f79ff185 100644 --- a/packages/bruno-common/src/utils/index.ts +++ b/packages/bruno-common/src/utils/index.ts @@ -1,5 +1,9 @@ export { encodeUrl, parseQueryParams, - buildQueryString + buildQueryString, } from './url'; + +export { + isPotentiallyTrustworthyOrigin +} from './url/validation'; \ No newline at end of file diff --git a/packages/bruno-requests/src/utils/cookie-utils.spec.js b/packages/bruno-common/src/utils/url/validation.spec.ts similarity index 98% rename from packages/bruno-requests/src/utils/cookie-utils.spec.js rename to packages/bruno-common/src/utils/url/validation.spec.ts index 90e8e74c3..1732898f5 100644 --- a/packages/bruno-requests/src/utils/cookie-utils.spec.js +++ b/packages/bruno-common/src/utils/url/validation.spec.ts @@ -1,4 +1,4 @@ -const { isPotentiallyTrustworthyOrigin } = require('./cookie-utils'); +import { isPotentiallyTrustworthyOrigin } from './validation'; describe('isPotentiallyTrustworthyOrigin', () => { describe('secure schemes', () => { @@ -130,4 +130,4 @@ describe('isPotentiallyTrustworthyOrigin', () => { expect(isPotentiallyTrustworthyOrigin('wss://localhost')).toBe(true); }); }); -}); +}); \ No newline at end of file diff --git a/packages/bruno-common/src/utils/url/validation.ts b/packages/bruno-common/src/utils/url/validation.ts new file mode 100644 index 000000000..1a94bcfdc --- /dev/null +++ b/packages/bruno-common/src/utils/url/validation.ts @@ -0,0 +1,67 @@ +import { isIPv4, isIPv6, isIP } from 'is-ip'; + +const hostNoBrackets = (host: string): string => { + if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) { + return host.substring(1, host.length - 1); + } + return host; +}; + +const isLoopbackV4 = (address: string): boolean => { + const octets = address.split('.'); + if (octets.length !== 4 || parseInt(octets[0], 10) !== 127) { + return false; + } + return octets.every((octet) => { + const n = parseInt(octet, 10); + return !Number.isNaN(n) && n >= 0 && n <= 255; + }); +}; + +const isLoopbackV6 = (address: string): boolean => address === '::1'; + +const isIpLoopback = (address: string): boolean => { + if (isIPv4(address)) { + return isLoopbackV4(address); + } + if (isIPv6(address)) { + return isLoopbackV6(address); + } + return false; +}; + +const isNormalizedLocalhostTLD = (host: string): boolean => host.toLowerCase().endsWith('.localhost'); + +const isLocalHostname = (host: string): boolean => { + return host.toLowerCase() === 'localhost' || isNormalizedLocalhostTLD(host); +}; + +/** + * Mirrors Chrome / Secure Contexts spec for "potentially trustworthy origins". + */ +const isPotentiallyTrustworthyOrigin = (urlString: string): boolean => { + let url: URL; + try { + url = new URL(urlString); + } catch { + return false; // invalid URL or opaque origin + } + + const scheme = url.protocol.replace(':', '').toLowerCase(); + const hostname = hostNoBrackets(url.hostname).replace(/\.+$/, ''); + + // Secure schemes + if (scheme === 'https' || scheme === 'wss' || scheme === 'file') { + return true; + } + + // IP literals + if (isIP(hostname)) { + return isIpLoopback(hostname); + } + + // localhost / *.localhost + return isLocalHostname(hostname); +}; + +export { isPotentiallyTrustworthyOrigin }; \ No newline at end of file diff --git a/packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js b/packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js new file mode 100644 index 000000000..c95dbaabe --- /dev/null +++ b/packages/bruno-common/tests/cookies/cookie-jar-wrapper.spec.js @@ -0,0 +1,228 @@ +const cookiesModule = require('../../src/cookies/index.ts').default; + +describe('Bruno Cookie Jar Wrapper - API Examples', () => { + let jar; + const testUrl = 'https://api.example.com'; + + beforeEach(() => { + jar = cookiesModule.jar(); + // Clear all cookies before each test + jar.clear(); + }); + + describe('Basic Cookie Operations', () => { + test('setCookie and getCookie - name/value pair', async () => { + const cookieName = 'authToken'; + const cookieValue = 'jwt123'; + + // Set a cookie + await jar.setCookie(testUrl, cookieName, cookieValue); + + // Get the cookie back + const cookie = await jar.getCookie(testUrl, cookieName); + expect(cookie.key).toBe(cookieName); + expect(cookie.value).toBe(cookieValue); + expect(cookie.domain).toBe('api.example.com'); + }); + + test('setCookie with cookie object', async () => { + const cookieObj = { + key: 'sessionId', + value: 'abc123', + path: '/api', + httpOnly: true, + secure: true + }; + + await jar.setCookie(testUrl, cookieObj); + + const cookie = await jar.getCookie(testUrl + '/api', 'sessionId'); + expect(cookie.key).toBe('sessionId'); + expect(cookie.value).toBe('abc123'); + expect(cookie.path).toBe('/api'); + expect(cookie.httpOnly).toBe(true); + expect(cookie.secure).toBe(true); + }); + + test('getCookie returns null for non-existent cookie', async () => { + const cookie = await jar.getCookie(testUrl, 'nonexistent'); + expect(cookie).toBeNull(); + }); + }); + + describe('Multiple Cookie Operations', () => { + test('setCookies with array of cookie objects', async () => { + const cookies = [ + { key: 'cookie1', value: 'value1' }, + { key: 'cookie2', value: 'value2' }, + { key: 'cookie3', value: 'value3', httpOnly: true } + ]; + + await jar.setCookies(testUrl, cookies); + + // Verify all cookies were set + const retrievedCookies = await jar.getCookies(testUrl); + expect(retrievedCookies).toHaveLength(3); + + const cookieNames = retrievedCookies.map(c => c.key); + expect(cookieNames).toContain('cookie1'); + expect(cookieNames).toContain('cookie2'); + expect(cookieNames).toContain('cookie3'); + }); + + test('getCookies returns all cookies for URL', async () => { + // Set multiple cookies + await jar.setCookie(testUrl, 'auth', 'token123'); + await jar.setCookie(testUrl, 'session', 'sess456'); + await jar.setCookie(testUrl, 'prefs', 'theme=dark'); + + const cookies = await jar.getCookies(testUrl); + expect(cookies).toHaveLength(3); + + const cookieMap = cookies.reduce((map, cookie) => { + map[cookie.key] = cookie.value; + return map; + }, {}); + + expect(cookieMap.auth).toBe('token123'); + expect(cookieMap.session).toBe('sess456'); + expect(cookieMap.prefs).toBe('theme=dark'); + }); + }); + + describe('Cookie Deletion', () => { + test('deleteCookie removes specific cookie', async () => { + // Set two cookies + await jar.setCookie(testUrl, 'keep', 'keepValue'); + await jar.setCookie(testUrl, 'remove', 'removeValue'); + + // Delete one cookie + await jar.deleteCookie(testUrl, 'remove'); + + // Verify only one cookie remains + const cookies = await jar.getCookies(testUrl); + expect(cookies).toHaveLength(1); + expect(cookies[0].key).toBe('keep'); + expect(cookies[0].value).toBe('keepValue'); + }); + + test('deleteCookies removes all cookies for URL', async () => { + // Set multiple cookies + await jar.setCookie(testUrl, 'cookie1', 'value1'); + await jar.setCookie(testUrl, 'cookie2', 'value2'); + + // Delete all cookies for the URL + await jar.deleteCookies(testUrl); + + // Verify no cookies remain + const cookies = await jar.getCookies(testUrl); + expect(cookies).toHaveLength(0); + }); + + test('clear removes all cookies from jar', async () => { + // Set cookies for multiple URLs + await jar.setCookie('https://site1.com', 'cookie1', 'value1'); + await jar.setCookie('https://site2.com', 'cookie2', 'value2'); + + // Clear entire jar + await jar.clear(); + + // Verify no cookies remain for any URL + const cookies1 = await jar.getCookies('https://site1.com'); + const cookies2 = await jar.getCookies('https://site2.com'); + + expect(cookies1).toHaveLength(0); + expect(cookies2).toHaveLength(0); + }); + }); + + describe('Error Handling', () => { + test('setCookie handles missing URL', async () => { + await expect(jar.setCookie('', 'name', 'value')).rejects.toThrow('URL is required'); + }); + + test('getCookie handles missing URL', async () => { + await expect(jar.getCookie('', 'name')).rejects.toThrow('URL and cookie name are required'); + }); + + test('setCookies handles invalid input', async () => { + await expect(jar.setCookies(testUrl, 'not-an-array')).rejects.toThrow('expects an array'); + }); + + test('setCookie handles missing cookie name in object', async () => { + await expect(jar.setCookie(testUrl, { value: 'test' })).rejects.toThrow('key (name) is required'); + }); + }); + + describe('Real-world Usage Examples', () => { + test('Authentication workflow example', async () => { + const apiUrl = 'https://api.example.com'; + const authToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'; + + // Simulate login - set auth cookie + await jar.setCookie(apiUrl, 'authToken', authToken); + + // Later in the session - retrieve auth token + const cookie = await jar.getCookie(apiUrl, 'authToken'); + expect(cookie.value).toBe(authToken); + + // Simulate logout - remove auth cookie + await jar.deleteCookie(apiUrl, 'authToken'); + + // Verify cookie is gone + const deletedCookie = await jar.getCookie(apiUrl, 'authToken'); + expect(deletedCookie).toBeNull(); + }); + + test('Session management with multiple cookies', async () => { + const sessionUrl = 'https://app.example.com'; + + // Set session cookies + const sessionCookies = [ + { key: 'sessionId', value: 'sess_123', httpOnly: true }, + { key: 'csrfToken', value: 'csrf_456' }, + { key: 'userPrefs', value: JSON.stringify({ theme: 'dark', lang: 'en' }) } + ]; + + await jar.setCookies(sessionUrl, sessionCookies); + + // Retrieve all session cookies + const cookies = await jar.getCookies(sessionUrl); + expect(cookies).toHaveLength(3); + + // Find specific cookies + const sessionCookie = cookies.find(c => c.key === 'sessionId'); + const csrfCookie = cookies.find(c => c.key === 'csrfToken'); + const prefsCookie = cookies.find(c => c.key === 'userPrefs'); + + expect(sessionCookie.value).toBe('sess_123'); + expect(sessionCookie.httpOnly).toBe(true); + expect(csrfCookie.value).toBe('csrf_456'); + + const prefs = JSON.parse(prefsCookie.value); + expect(prefs.theme).toBe('dark'); + expect(prefs.lang).toBe('en'); + }); + + test('Cookie path handling', async () => { + const baseUrl = 'https://example.com'; + + // Set cookies with different paths + await jar.setCookie(baseUrl, { key: 'global', value: 'global_val', path: '/' }); + await jar.setCookie(baseUrl, { key: 'api', value: 'api_val', path: '/api' }); + await jar.setCookie(baseUrl, { key: 'admin', value: 'admin_val', path: '/admin' }); + + const rootCookies = await jar.getCookies(baseUrl + '/'); + const globalCookie = rootCookies.find(c => c.key === 'global'); + expect(globalCookie).toBeTruthy(); + expect(globalCookie.value).toBe('global_val'); + + const apiCookies = await jar.getCookies(baseUrl + '/api/users'); + expect(apiCookies.length).toBeGreaterThanOrEqual(2); + + const apiCookieNames = apiCookies.map(c => c.key); + expect(apiCookieNames).toContain('global'); + expect(apiCookieNames).toContain('api'); + }); + }); +}); \ No newline at end of file diff --git a/packages/bruno-converters/src/postman/postman-translations.js b/packages/bruno-converters/src/postman/postman-translations.js index 9dc48a5cd..43c034db9 100644 --- a/packages/bruno-converters/src/postman/postman-translations.js +++ b/packages/bruno-converters/src/postman/postman-translations.js @@ -48,6 +48,13 @@ const replacements = { 'pm\\.execution\\.skipRequest': 'bru.runner.skipRequest', 'pm\\.execution\\.setNextRequest\\(null\\)': 'bru.runner.stopExecution()', 'pm\\.execution\\.setNextRequest\\(\'null\'\\)': 'bru.runner.stopExecution()', + // Cookie jar translations + 'pm\\.cookies\\.jar\\(\\)': 'bru.cookies.jar()', + 'pm\\.cookies\\.jar\\(\\)\\.get\\(': 'bru.cookies.jar().getCookie(', + 'pm\\.cookies\\.jar\\(\\)\\.set\\(': 'bru.cookies.jar().setCookie(', + 'pm\\.cookies\\.jar\\(\\)\\.unset\\(': 'bru.cookies.jar().deleteCookie(', + 'pm\\.cookies\\.jar\\(\\)\\.clear\\(': 'bru.cookies.jar().deleteCookies(', + 'pm\\.cookies\\.jar\\(\\)\\.getAll\\(': 'bru.cookies.jar().getCookies(', }; const extendedReplacements = Object.keys(replacements).reduce((acc, key) => { diff --git a/packages/bruno-converters/src/utils/jscode-shift-translator.js b/packages/bruno-converters/src/utils/jscode-shift-translator.js index 6b0cea683..405a8d540 100644 --- a/packages/bruno-converters/src/utils/jscode-shift-translator.js +++ b/packages/bruno-converters/src/utils/jscode-shift-translator.js @@ -14,6 +14,11 @@ function getMemberExpressionString(node) { return node.name; } + if (node.type === 'CallExpression') { + const calleeStr = getMemberExpressionString(node.callee); + return `${calleeStr}()`; + } + // Handle member expressions if (node.type === 'MemberExpression') { const objectStr = getMemberExpressionString(node.object); @@ -89,6 +94,13 @@ const simpleTranslations = { 'pm.response.size().body': 'res.getSize().body', 'pm.response.size().header': 'res.getSize().header', 'pm.response.size().total': 'res.getSize().total', + 'pm.cookies.jar': 'bru.cookies.jar', + + 'pm.cookies.jar().get': 'bru.cookies.jar().getCookie', + 'pm.cookies.jar().getAll': 'bru.cookies.jar().getCookies', + 'pm.cookies.jar().set': 'bru.cookies.jar().setCookie', + 'pm.cookies.jar().unset': 'bru.cookies.jar().deleteCookie', + 'pm.cookies.jar().clear': 'bru.cookies.jar().deleteCookies', // Execution control 'pm.execution.skipRequest': 'bru.runner.skipRequest', @@ -332,6 +344,9 @@ function translateCode(code) { // Preprocess the code to resolve all aliases preprocessAliases(ast); + // Handle cookie jar variable assignments and method renaming + processCookieJarVariables(ast); + // Process all transformations in a single pass processTransformations(ast, transformedNodes); @@ -610,6 +625,59 @@ function removeResolvedDeclarations(ast, symbolTable) { return changesMade; } +/** + * Process cookie jar variable assignments and rename methods on those variables + * @param {Object} ast - jscodeshift AST + */ +function processCookieJarVariables(ast) { + // Map of Postman cookie jar method names to Bruno equivalents + const cookieMethodMapping = { + 'get': 'getCookie', + 'getAll': 'getCookies', + 'set': 'setCookie', + 'unset': 'deleteCookie', + 'clear': 'deleteCookies' + }; + + // Track variables that are assigned to cookie jar instances + const cookieJarVariables = new Set(); + + // First pass: Find all variables assigned to cookie jar instances + ast.find(j.VariableDeclarator).forEach(path => { + if (path.value.init && path.value.init.type === 'CallExpression') { + const initCall = path.value.init; + + // Check if this is a cookie jar assignment + if (initCall.callee.type === 'MemberExpression') { + const calleeStr = getMemberExpressionString(initCall.callee); + + if (calleeStr === 'pm.cookies.jar' || calleeStr === 'bru.cookies.jar') { + if (path.value.id.type === 'Identifier') { + cookieJarVariables.add(path.value.id.name); + } + } + } + } + }); + + // Second pass: Rename method calls on cookie jar variables + ast.find(j.CallExpression).forEach(path => { + if (path.value.callee.type === 'MemberExpression' && + path.value.callee.object.type === 'Identifier' && + path.value.callee.property.type === 'Identifier') { + + const varName = path.value.callee.object.name; + const methodName = path.value.callee.property.name; + + // If this is a method call on a cookie jar variable + if (cookieJarVariables.has(varName) && cookieMethodMapping[methodName]) { + const newMethodName = cookieMethodMapping[methodName]; + path.value.callee.property.name = newMethodName; + } + } + }); +} + /** * Handle Postman's tests["..."] = ... syntax * @param {Object} ast - jscodeshift AST diff --git a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js index fed9f2931..ba3f29d37 100644 --- a/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js +++ b/packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js @@ -16,8 +16,8 @@ describe('postmanTranslations - comment handling', () => { }); test('should comment non-translated pm commands', () => { - const inputScript = "pm.test('random test', () => pm.cookies.get('cookieName'));"; - const expectedOutput = "// test('random test', () => pm.cookies.get('cookieName'));"; + const inputScript = "pm.test('random test', () => pm.globals.clear());"; + const expectedOutput = "// test('random test', () => pm.globals.clear());"; expect(postmanTranslation(inputScript)).toBe(expectedOutput); }); diff --git a/packages/bruno-converters/tests/postman/postman-translations/postman-cookie-conversions.spec.js b/packages/bruno-converters/tests/postman/postman-translations/postman-cookie-conversions.spec.js new file mode 100644 index 000000000..e4fe0ee32 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/postman-cookie-conversions.spec.js @@ -0,0 +1,319 @@ +const { default: postmanTranslation } = require("../../../src/postman/postman-translations"); + +describe('postmanTranslations - cookie API conversions', () => { + test('should convert pm.cookies.jar().get to bru.cookies.jar().getCookie', () => { + const inputScript = `pm.cookies.jar().get('https://example.com', 'sessionId', (err, cookie) => { + console.log(cookie); + });`; + + const expectedOutput = `bru.cookies.jar().getCookie('https://example.com', 'sessionId', (err, cookie) => { + console.log(cookie); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert pm.cookies.jar().getAll to bru.cookies.jar().getCookies', () => { + const inputScript = `pm.cookies.jar().getAll('https://example.com', (err, cookies) => { + console.log(cookies); + });`; + + const expectedOutput = `bru.cookies.jar().getCookies('https://example.com', (err, cookies) => { + console.log(cookies); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert pm.cookies.jar().set to bru.cookies.jar().setCookie', () => { + const inputScript = `pm.cookies.jar().set('https://example.com', 'sessionId', 'abc123', (err) => { + if (err) console.error(err); + });`; + + const expectedOutput = `bru.cookies.jar().setCookie('https://example.com', 'sessionId', 'abc123', (err) => { + if (err) console.error(err); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert pm.cookies.jar().unset to bru.cookies.jar().deleteCookie', () => { + const inputScript = `pm.cookies.jar().unset('https://example.com', 'sessionId', (err) => { + if (err) console.error(err); + });`; + + const expectedOutput = `bru.cookies.jar().deleteCookie('https://example.com', 'sessionId', (err) => { + if (err) console.error(err); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert pm.cookies.jar().clear to bru.cookies.jar().deleteCookies (behavior difference)', () => { + const inputScript = `pm.cookies.jar().clear('https://example.com', (err) => { + if (err) console.error(err); + });`; + + const expectedOutput = `bru.cookies.jar().deleteCookies('https://example.com', (err) => { + if (err) console.error(err); + });`; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should handle multiple cookie operations in one script', () => { + const inputScript = ` + pm.cookies.jar().set('https://api.example.com', 'auth', 'token123'); + const cookie = pm.cookies.jar().get('https://api.example.com', 'auth'); + pm.cookies.jar().getAll('https://api.example.com', (err, cookies) => { + console.log('All cookies:', cookies); + }); + pm.cookies.jar().unset('https://api.example.com', 'temp'); + pm.cookies.jar().clear('https://api.example.com'); + `; + + const expectedOutput = ` + bru.cookies.jar().setCookie('https://api.example.com', 'auth', 'token123'); + const cookie = bru.cookies.jar().getCookie('https://api.example.com', 'auth'); + bru.cookies.jar().getCookies('https://api.example.com', (err, cookies) => { + console.log('All cookies:', cookies); + }); + bru.cookies.jar().deleteCookie('https://api.example.com', 'temp'); + bru.cookies.jar().deleteCookies('https://api.example.com'); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert variable assignment and method calls on cookie jar variables', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.set('https://example.com', 'user', 'john'); + const userCookie = jar.get('https://example.com', 'user'); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.setCookie('https://example.com', 'user', 'john'); + const userCookie = jar.getCookie('https://example.com', 'user'); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.get to jar.getCookie with callback', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.get('https://api.example.com', 'authToken', (error, cookie) => { + if (error) { + console.error('Error getting cookie:', error); + } else { + console.log('Retrieved cookie:', cookie); + } + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.getCookie('https://api.example.com', 'authToken', (error, cookie) => { + if (error) { + console.error('Error getting cookie:', error); + } else { + console.log('Retrieved cookie:', cookie); + } + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.getAll to jar.getCookies with callback', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.getAll('https://api.example.com', (error, cookies) => { + if (error) { + console.error('Error getting cookies:', error); + } else { + console.log('All cookies:', cookies); + } + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.getCookies('https://api.example.com', (error, cookies) => { + if (error) { + console.error('Error getting cookies:', error); + } else { + console.log('All cookies:', cookies); + } + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.set to jar.setCookie with cookie object', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.set('https://api.example.com', { + key: 'sessionId', + value: 'abc123', + path: '/api', + httpOnly: true, + secure: true + }, (error) => { + if (error) console.error(error); + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.setCookie('https://api.example.com', { + key: 'sessionId', + value: 'abc123', + path: '/api', + httpOnly: true, + secure: true + }, (error) => { + if (error) console.error(error); + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.unset to jar.deleteCookie', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.unset('https://api.example.com', 'tempCookie', (error) => { + if (error) { + console.error('Failed to delete cookie:', error); + } else { + console.log('Cookie deleted successfully'); + } + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.deleteCookie('https://api.example.com', 'tempCookie', (error) => { + if (error) { + console.error('Failed to delete cookie:', error); + } else { + console.log('Cookie deleted successfully'); + } + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should convert jar.clear to jar.deleteCookies', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.clear('https://api.example.com', (error) => { + if (error) { + console.error('Failed to clear cookies:', error); + } else { + console.log('All cookies cleared for domain'); + } + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.deleteCookies('https://api.example.com', (error) => { + if (error) { + console.error('Failed to clear cookies:', error); + } else { + console.log('All cookies cleared for domain'); + } + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should handle complex cookie workflow with jar variable', () => { + const inputScript = ` + const cookieJar = pm.cookies.jar(); + + // Set multiple cookies + cookieJar.set('https://example.com', 'auth', 'token123'); + cookieJar.set('https://example.com', { + key: 'preferences', + value: JSON.stringify({theme: 'dark'}), + path: '/' + }); + + // Get specific cookie + cookieJar.get('https://example.com', 'auth', (err, authCookie) => { + console.log('Auth cookie:', authCookie); + }); + + // Get all cookies + cookieJar.getAll('https://example.com', (err, allCookies) => { + console.log('Total cookies:', allCookies.length); + }); + + // Clean up + cookieJar.unset('https://example.com', 'temp'); + cookieJar.clear('https://example.com'); + `; + + const expectedOutput = ` + const cookieJar = bru.cookies.jar(); + + // Set multiple cookies + cookieJar.setCookie('https://example.com', 'auth', 'token123'); + cookieJar.setCookie('https://example.com', { + key: 'preferences', + value: JSON.stringify({theme: 'dark'}), + path: '/' + }); + + // Get specific cookie + cookieJar.getCookie('https://example.com', 'auth', (err, authCookie) => { + console.log('Auth cookie:', authCookie); + }); + + // Get all cookies + cookieJar.getCookies('https://example.com', (err, allCookies) => { + console.log('Total cookies:', allCookies.length); + }); + + // Clean up + cookieJar.deleteCookie('https://example.com', 'temp'); + cookieJar.deleteCookies('https://example.com'); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); + + test('should handle mixed jar variable and direct calls', () => { + const inputScript = ` + const jar = pm.cookies.jar(); + jar.get('https://api.com', 'session'); + + pm.cookies.jar().set('https://other.com', 'temp', 'value'); + + jar.getAll('https://api.com', (err, cookies) => { + console.log(cookies); + }); + `; + + const expectedOutput = ` + const jar = bru.cookies.jar(); + jar.getCookie('https://api.com', 'session'); + + bru.cookies.jar().setCookie('https://other.com', 'temp', 'value'); + + jar.getCookies('https://api.com', (err, cookies) => { + console.log(cookies); + }); + `; + + expect(postmanTranslation(inputScript)).toBe(expectedOutput); + }); +}); \ No newline at end of file diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index b398cc9bf..cdfd6bcb7 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -32,13 +32,13 @@ "@aws-sdk/credential-providers": "3.750.0", "@usebruno/common": "0.1.0", "@usebruno/converters": "^0.1.0", + "@usebruno/filestore": "^0.1.0", "@usebruno/js": "0.12.0", "@usebruno/lang": "0.12.0", "@usebruno/node-machine-id": "^2.0.0", + "@usebruno/requests": "^0.1.0", "@usebruno/schema": "0.7.0", "@usebruno/vm2": "^3.9.13", - "@usebruno/requests": "^0.1.0", - "@usebruno/filestore": "^0.1.0", "about-window": "^1.15.2", "aws4-axios": "^3.3.0", "axios": "^1.8.3", @@ -65,7 +65,6 @@ "nanoid": "3.3.8", "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", - "tough-cookie": "^4.1.3", "uuid": "^9.0.0", "yup": "^0.32.11" }, diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 8e39e0d5c..6e12126b3 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -472,6 +472,9 @@ const registerNetworkIpc = (mainWindow) => { }); collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables; + + const domainsWithCookies = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); } // interpolate variables inside request @@ -584,6 +587,9 @@ const registerNetworkIpc = (mainWindow) => { }); collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables; + + const domainsWithCookiesPost = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPost))); } return scriptResult; }; @@ -891,6 +897,9 @@ const registerNetworkIpc = (mainWindow) => { scriptType: 'test', error: testError }); + + const domainsWithCookiesTest = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest))); } return { @@ -1094,6 +1103,9 @@ const registerNetworkIpc = (mainWindow) => { error: preRequestError }); + const domainsWithCookiesPreRequest = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPreRequest))); + if (preRequestError) { throw preRequestError; } @@ -1282,6 +1294,9 @@ const registerNetworkIpc = (mainWindow) => { error: postResponseError }); + const domainsWithCookiesPostResponse = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPostResponse))); + if (postResponseScriptResult?.nextRequestName !== undefined) { nextRequestName = postResponseScriptResult.nextRequestName; } @@ -1386,6 +1401,9 @@ const registerNetworkIpc = (mainWindow) => { scriptType: 'test', error: testError }); + + const domainsWithCookiesTest = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest))); } } catch (error) { mainWindow.webContents.send('main:run-folder-event', { diff --git a/packages/bruno-electron/src/utils/cookies.js b/packages/bruno-electron/src/utils/cookies.js index 7f3751eaf..f4aaef547 100644 --- a/packages/bruno-electron/src/utils/cookies.js +++ b/packages/bruno-electron/src/utils/cookies.js @@ -1,197 +1 @@ -const { Cookie, CookieJar } = require('tough-cookie'); -const each = require('lodash/each'); -const moment = require('moment'); -const { isPotentiallyTrustworthyOrigin } = require('@usebruno/requests').utils; - -const cookieJar = new CookieJar(); - -const addCookieToJar = (setCookieHeader, requestUrl) => { - const cookie = Cookie.parse(setCookieHeader, { loose: true }); - cookieJar.setCookieSync(cookie, requestUrl, { - ignoreError: true // silently ignore things like parse errors and invalid domains - }); -}; - -const getCookiesForUrl = (url) => { - return cookieJar.getCookiesSync(url, { - secure: isPotentiallyTrustworthyOrigin(url) - }); -}; - -const getCookieStringForUrl = (url) => { - const cookies = getCookiesForUrl(url); - - if (!Array.isArray(cookies) || !cookies.length) { - return ''; - } - - const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); - - return validCookies.map((cookie) => cookie.cookieString()).join('; '); -}; - -const getDomainsWithCookies = () => { - return new Promise((resolve, reject) => { - const domainCookieMap = {}; - - cookieJar.store.getAllCookies((err, cookies) => { - if (err) { - return reject(err); - } - - cookies.forEach((cookie) => { - if (!domainCookieMap[cookie.domain]) { - domainCookieMap[cookie.domain] = [cookie]; - } else { - domainCookieMap[cookie.domain].push(cookie); - } - }); - - const domains = Object.keys(domainCookieMap); - const domainsWithCookies = []; - - each(domains, (domain) => { - const cookies = domainCookieMap[domain]; - const validCookies = cookies.filter((cookie) => !cookie.expires || cookie.expires > Date.now()); - - if (validCookies.length) { - domainsWithCookies.push({ - domain, - cookies: validCookies, - cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ') - }); - } - }); - - resolve(domainsWithCookies); - }); - }); -}; - -const deleteCookie = (domain, path, cookieKey) => { - return new Promise((resolve, reject) => { - cookieJar.store.removeCookie(domain, path, cookieKey, (err) => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -}; - -const deleteCookiesForDomain = (domain) => { - return new Promise((resolve, reject) => { - cookieJar.store.removeCookies(domain, null, (err) => { - if (err) { - return reject(err); - } - return resolve(); - }); - }); -}; - -const updateCookieObj = (cookieObj, oldCookie) => { - return { - ...cookieObj, - // Preserve immutable properties from old cookie - path: oldCookie.path, - key: oldCookie.key, - domain: oldCookie.domain, - // Handle other mutable properties - expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity, - creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(), - lastAccessed: - oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid() - ? new Date(oldCookie.lastAccessed) - : new Date() - }; -}; - -const createCookieObj = (cookieObj) => { - return { - ...cookieObj, - path: cookieObj.path || '/', - expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity, - creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(), - lastAccessed: - cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid() - ? new Date(cookieObj.lastAccessed) - : new Date() - }; -}; - -const addCookieForDomain = (domain, cookieObj) => { - return new Promise((resolve, reject) => { - try { - const cookie = new Cookie(createCookieObj(cookieObj)); - cookieJar.store.putCookie(cookie, (err) => { - if (err) { - return reject(err); - } - return resolve(); - }); - } catch (err) { - reject(err); - } - }); -}; - -const modifyCookieForDomain = (domain, oldCookieObj, cookieObj) => { - return new Promise((resolve, reject) => { - try { - const oldCookie = new Cookie(createCookieObj(oldCookieObj)); - const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie)); - cookieJar.store.updateCookie(oldCookie, newCookie, (removeErr) => { - if (removeErr) { - return reject(removeErr); - } - return resolve(); - }); - } catch (err) { - reject(err); - } - }); -}; - -const parseCookieString = (cookieStr) => { - try { - const cookie = Cookie.parse(cookieStr); - if (!cookie) return null; - - return { - ...cookie, - expires: cookie.expires === Infinity ? null : cookie.expires - }; - } catch (err) { - throw new Error(err); - } -}; - -const createCookieString = (cookieObj) => { - const cookie = new Cookie(createCookieObj(cookieObj)); - - // cookie.toString() omits the domain - let cookieString = cookie.toString(); - - // Manually append domain and hostOnly if they exist - if (cookieObj.hostOnly && !cookieString.includes('Domain=')) { - cookieString += `; Domain=${cookieObj.domain}`; - } - - return cookieString; -}; - -module.exports = { - addCookieToJar, - getCookiesForUrl, - getCookieStringForUrl, - getDomainsWithCookies, - deleteCookie, - deleteCookiesForDomain, - addCookieForDomain, - modifyCookieForDomain, - parseCookieString, - createCookieString, - updateCookieObj, - createCookieObj -}; +module.exports = require('@usebruno/common').cookies; diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index 5dad6935e..c00ac9b55 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -1,6 +1,7 @@ const { cloneDeep } = require('lodash'); const { interpolate: _interpolate } = require('@usebruno/common'); const { sendRequest } = require('@usebruno/requests').scripting; +const { jar: createCookieJar } = require('@usebruno/common').cookies; const variableNameRegex = /^[\w-.]*$/; @@ -17,6 +18,50 @@ class Bru { this.collectionPath = collectionPath; this.collectionName = collectionName; this.sendRequest = sendRequest; + + this.cookies = { + jar: () => { + const cookieJar = createCookieJar(); + + return { + getCookie: (url, cookieName, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.getCookie(interpolatedUrl, cookieName, callback); + }, + + getCookies: (url, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.getCookies(interpolatedUrl, callback); + }, + + setCookie: (url, nameOrCookieObj, valueOrCallback, maybeCallback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.setCookie(interpolatedUrl, nameOrCookieObj, valueOrCallback, maybeCallback); + }, + + setCookies: (url, cookiesArray, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.setCookies(interpolatedUrl, cookiesArray, callback); + }, + + // Clear entire cookie jar + clear: (callback) => { + return cookieJar.clear(callback); + }, + + // Delete cookies for a specific URL/domain + deleteCookies: (url, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.deleteCookies(interpolatedUrl, callback); + }, + + deleteCookie: (url, cookieName, callback) => { + const interpolatedUrl = this.interpolate(url); + return cookieJar.deleteCookie(interpolatedUrl, cookieName, callback); + } + }; + } + }; this.runner = { skipRequest: () => { this.skipRequest = true; diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index 5be5e26d0..d99aec94b 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -258,6 +258,136 @@ const addBruShimToContext = (vm, bru) => { }); sleep.consume((handle) => vm.setProp(bruObject, 'sleep', handle)); + let bruCookiesObject = vm.newObject(); + + const _jarFn = vm.newFunction('_jar', () => { + const nativeJar = bru.cookies.jar(); + const jarObj = vm.newObject(); + + const _getCookieFn = vm.newFunction('_getCookie', (url, cookieName) => { + const promise = vm.newPromise(); + nativeJar.getCookie(vm.dump(url), vm.dump(cookieName), (err, cookie) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(marshallToVm(cleanCircularJson(cookie), vm)); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _getCookieFn.consume((handle) => vm.setProp(jarObj, '_getCookie', handle)); + + const _getCookiesFn = vm.newFunction('_getCookies', (url) => { + const promise = vm.newPromise(); + nativeJar.getCookies(vm.dump(url), (err, cookies) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(marshallToVm(cleanCircularJson(cookies), vm)); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _getCookiesFn.consume((handle) => vm.setProp(jarObj, '_getCookies', handle)); + + const _setCookieFn = vm.newFunction('_setCookie', (url, nameOrCookieObj, value) => { + const promise = vm.newPromise(); + const dumpedUrl = vm.dump(url); + const dumpedNameOrObj = vm.dump(nameOrCookieObj); + + // Check if the second argument is an object (cookie object case) + if (typeof dumpedNameOrObj === 'object' && dumpedNameOrObj !== null) { + // Cookie object case: setCookie(url, cookieObject, callback) + nativeJar.setCookie(dumpedUrl, dumpedNameOrObj, (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + } else { + // Name/value case: setCookie(url, name, value, callback) + const dumpedValue = value ? vm.dump(value) : ''; + nativeJar.setCookie(dumpedUrl, dumpedNameOrObj, dumpedValue, (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + } + + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _setCookieFn.consume((handle) => vm.setProp(jarObj, '_setCookie', handle)); + + const _setCookiesFn = vm.newFunction('_setCookies', (url, cookiesArray) => { + const promise = vm.newPromise(); + + nativeJar.setCookies(vm.dump(url), vm.dump(cookiesArray), (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _setCookiesFn.consume((handle) => vm.setProp(jarObj, '_setCookies', handle)); + + const _clearFn = vm.newFunction('_clear', () => { + const promise = vm.newPromise(); + nativeJar.clear((err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _clearFn.consume((handle) => vm.setProp(jarObj, '_clear', handle)); + + const _deleteCookiesFn = vm.newFunction('_deleteCookies', (url) => { + const promise = vm.newPromise(); + nativeJar.deleteCookies(vm.dump(url), (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _deleteCookiesFn.consume((handle) => vm.setProp(jarObj, '_deleteCookies', handle)); + + const _deleteCookieFn = vm.newFunction('_deleteCookie', (url, cookieName) => { + const promise = vm.newPromise(); + nativeJar.deleteCookie(vm.dump(url), vm.dump(cookieName), (err) => { + if (err) { + promise.reject(marshallToVm(cleanJson(err), vm)); + } else { + promise.resolve(vm.undefined); + } + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + _deleteCookieFn.consume((handle) => vm.setProp(jarObj, '_deleteCookie', handle)); + + return jarObj; + }); + _jarFn.consume((handle) => vm.setProp(bruCookiesObject, '_jar', handle)); + + vm.setProp(bruObject, 'cookies', bruCookiesObject); + bruCookiesObject.dispose(); + vm.setProp(bruObject, 'runner', bruRunnerObject); vm.setProp(vm.global, 'bru', bruObject); bruObject.dispose(); @@ -282,7 +412,41 @@ const addBruShimToContext = (vm, bru) => { return Promise.reject(err); } } - } + }; + + globalThis.bru.cookies.jar = () => { + const _jar = globalThis.bru.cookies._jar(); + + const callWithCallback = async (promiseFn, callback) => { + if (!callback) return await promiseFn(); + try { + const result = await promiseFn(); + try { await callback(null, result); } catch(cbErr) { return Promise.reject(cbErr); } + } catch(err) { + try { await callback(err, null); } catch(cbErr) { return Promise.reject(cbErr); } + } + }; + + return { + getCookie: (url, name, cb) => callWithCallback(() => _jar._getCookie(url, name), cb), + getCookies: (url, cb) => callWithCallback(() => _jar._getCookies(url), cb), + setCookie: (url, nameOrCookieObj, valueOrCallback, maybeCallback) => { + if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) { + const callback = typeof valueOrCallback === 'function' ? valueOrCallback : undefined; + return callWithCallback(() => _jar._setCookie(url, nameOrCookieObj), callback); + } else { + const value = typeof valueOrCallback === 'string' ? valueOrCallback : ''; + const callback = typeof maybeCallback === 'function' ? maybeCallback : + (typeof valueOrCallback === 'function' ? valueOrCallback : undefined); + return callWithCallback(() => _jar._setCookie(url, nameOrCookieObj, value), callback); + } + }, + setCookies: (url, cookiesArray, cb) => callWithCallback(() => _jar._setCookies(url, cookiesArray), cb), + clear: (cb) => callWithCallback(() => _jar._clear(), cb), + deleteCookies: (url, cb) => callWithCallback(() => _jar._deleteCookies(url), cb), + deleteCookie: (url, name, cb) => callWithCallback(() => _jar._deleteCookie(url, name), cb) + }; + }; `); }; diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index 3bdef99a0..b56f22347 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -1,7 +1,5 @@ export { addDigestInterceptor, getOAuth2Token } from './auth'; -export * as utils from './utils'; - export * as network from './network'; export * as scripting from './scripting'; \ No newline at end of file diff --git a/packages/bruno-requests/src/utils/cookie-utils.js b/packages/bruno-requests/src/utils/cookie-utils.js deleted file mode 100644 index 6a1a5ac57..000000000 --- a/packages/bruno-requests/src/utils/cookie-utils.js +++ /dev/null @@ -1,105 +0,0 @@ -const { URL } = require('node:url'); -const net = require('node:net'); - -const isLoopbackV4 = (address) => { - // 127.0.0.0/8: first octet = 127 - const octets = address.split('.'); - return ( - octets.length === 4 - ) && parseInt(octets[0], 10) === 127; -} - -const isLoopbackV6 = (address) => { - // new URL(...) follows the WHATWG URL Standard - // which compresses IPv6 addresses, therefore the IPv6 - // loopback address will always be compressed to '[::1]': - // https://url.spec.whatwg.org/#concept-ipv6-serializer - return (address === '::1'); -} - -const isIpLoopback = (address) => { - if (net.isIPv4(address)) { - return isLoopbackV4(address); - } - - if (net.isIPv6(address)) { - return isLoopbackV6(address); - } - - return false; -} - -const isNormalizedLocalhostTLD = (host) => { - return host.toLowerCase().endsWith('.localhost'); -} - -const isLocalHostname = (host) => { - return host.toLowerCase() === 'localhost' || - isNormalizedLocalhostTLD(host); -} - -/** - * Removes leading and trailing square brackets if present. - * Adapted from https://github.com/chromium/chromium/blob/main/url/gurl.cc#L440-L448 - * - * @param {string} host - * @returns {string} - */ -const hostNoBrackets = (host) => { - if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) { - return host.substring(1, host.length - 1); - } - return host; -} - -/** - * Determines if a URL string represents a potentially trustworthy origin. - * - * A URL is considered potentially trustworthy if it: - * - Uses HTTPS, WSS or file schemes - * - Points to a loopback address (IPv4 127.0.0.0/8 or IPv6 ::1) - * - Uses localhost or *.localhost hostnames - * - * @param {string} urlString - The URL to check - * @returns {boolean} - * @see {@link https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin W3C Spec} - */ -const isPotentiallyTrustworthyOrigin = (urlString) => { - let url; - - // try ... catch doubles as an opaque origin check - try { - url = new URL(urlString); - } catch (e) { - if (e instanceof TypeError && e.code === 'ERR_INVALID_URL') { - return false; - } else throw e; - } - - const scheme = url.protocol.replace(':', '').toLowerCase(); - const hostname = hostNoBrackets( - url.hostname - ).replace(/\.+$/, ''); - - if ( - scheme === 'https' || - scheme === 'wss' || - scheme === 'file' // https://w3c.github.io/webappsec-secure-contexts/#potentially-trustworthy-origin - ) { - return true; - } - - // If it's already an IP literal, check if it's a loopback address - if (net.isIP(hostname)) { - return isIpLoopback(hostname); - } - - // RFC 6761 states that localhost names will always resolve - // to the respective IP loopback address: - // https://datatracker.ietf.org/doc/html/rfc6761#section-6.3 - return isLocalHostname(hostname); -} - -module.exports = { - isPotentiallyTrustworthyOrigin -}; \ No newline at end of file diff --git a/packages/bruno-requests/src/utils/index.ts b/packages/bruno-requests/src/utils/index.ts deleted file mode 100644 index dd94dd186..000000000 --- a/packages/bruno-requests/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './cookie-utils'; diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/clear.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/clear.bru new file mode 100644 index 000000000..2f0000b3d --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/clear.bru @@ -0,0 +1,67 @@ +meta { + name: clear + type: http + seq: 6 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + await jar.setCookies('https://testbench-sanity.usebruno.com', [ + { + key: 'test_cookie_1', + value: 'value1', + path: '/', + secure: true + }, + { + key: 'test_cookie_2', + value: 'value2', + path: '/', + secure: true + } + ]); + + console.log("Test cookies set up for clear test"); +} + +script:post-response { + const jar = bru.cookies.jar() + + const cookiesBeforeClear = await jar.getCookies('https://testbench-sanity.usebruno.com'); + console.log(`Found ${cookiesBeforeClear.length} cookies before clearing`); + + test("cookies should exist before clearing", function() { + expect(cookiesBeforeClear).to.be.an('array'); + expect(cookiesBeforeClear.length).to.be.greaterThan(0); + }); + + await jar.clear(); + console.log("Cookie jar cleared"); +} + +tests { + const jar = bru.cookies.jar() + + test("should have no cookies after clearing", async function() { + const cookiesAfterClear = await jar.getCookies('https://testbench-sanity.usebruno.com'); + expect(cookiesAfterClear).to.be.an('array'); + expect(cookiesAfterClear.length).to.equal(0); + }); + + jar.clear(function(error) { + test("should successfully clear with callback", function() { + expect(error).to.be.null; + }); + }); +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookie.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookie.bru new file mode 100644 index 000000000..d1d1da1c2 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookie.bru @@ -0,0 +1,75 @@ +meta { + name: deleteCookie + type: http + seq: 5 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + await jar.setCookies('https://testbench-sanity.usebruno.com', [ + { + key: 'cookie_to_delete', + value: 'will_be_deleted', + path: '/', + secure: true + }, + { + key: 'cookie_to_keep', + value: 'should_remain', + path: '/', + secure: true + } + ]); + + console.log("Test cookies set up"); +} + +script:post-response { + const jar = bru.cookies.jar() + + const cookiesBefore = await jar.getCookies('https://testbench-sanity.usebruno.com'); + console.log(`Found ${cookiesBefore.length} cookies before deletion`); + + const targetCookie = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete'); + test("cookie should exist before deletion", function() { + expect(targetCookie).to.not.be.null; + expect(targetCookie.key).to.equal('cookie_to_delete'); + }); + + await jar.deleteCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete'); + console.log("Cookie deleted"); +} + +tests { + const jar = bru.cookies.jar() + + test("should have deleted the target cookie", async function() { + const deletedCookie = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete'); + expect(deletedCookie).to.be.null; + }); + + test("should keep other cookies intact", async function() { + const cookieToKeep = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_keep'); + expect(cookieToKeep).to.not.be.null; + expect(cookieToKeep.key).to.equal('cookie_to_keep'); + }); + + jar.deleteCookie("https://testbench-sanity.usebruno.com", "cookie_to_keep", function(error) { + test("should successfully delete with callback", function() { + expect(error).to.be.null; + }); + }); + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookies.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookies.bru new file mode 100644 index 000000000..03e604e8c --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookies.bru @@ -0,0 +1,106 @@ +meta { + name: deleteCookies + type: http + seq: 7 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + // Set up test cookies before the request + try { + await jar.setCookies('https://testbench-sanity.usebruno.com', [ + { + key: 'test_cookie_1', + value: 'value1', + path: '/', + httpOnly: false, + secure: true + }, + { + key: 'test_cookie_2', + value: 'value2', + path: '/', + httpOnly: true, + secure: true + }, + { + key: 'test_cookie_3', + value: 'value3', + path: '/api', + httpOnly: false, + secure: true + } + ]); + + console.log("Test cookies set up successfully in pre-request script"); + + // Verify cookies were set + const cookies = await jar.getCookies('https://testbench-sanity.usebruno.com'); + console.log(`${cookies.length} cookies set for domain`); + + } catch (error) { + console.error("Failed to set up test cookies:", error); + throw new Error(`Pre-request cookie setup failed: ${error.message || error}`); + } +} + +script:post-response { + const jar = bru.cookies.jar() + + // Verify cookies exist before deletion + try { + const cookiesBeforeDeletion = await jar.getCookies('https://testbench-sanity.usebruno.com'); + + test("cookies should exist before clearing", function() { + expect(cookiesBeforeDeletion).to.be.an('array'); + expect(cookiesBeforeDeletion.length).to.be.greaterThan(0); + }); + + + if (cookiesBeforeDeletion.length === 0) { + throw new Error("No cookies found to delete - setup may have failed"); + } + + // Delete all cookies for the domain + await jar.deleteCookies('https://testbench-sanity.usebruno.com'); + console.log("deleteCookies operation completed in post-response"); + + // Verify deletion worked + const cookiesAfterDeletion = await jar.getCookies('https://testbench-sanity.usebruno.com'); + console.log(`Found ${cookiesAfterDeletion.length} cookies after deletion`); + + } catch (error) { + console.error("Delete cookies error in post-response:", error); + throw new Error(`Failed to delete cookies in post-response: ${error.message || error}`); + } +} + +tests { + const jar = bru.cookies.jar() + + jar.getCookies("https://testbench-sanity.usebruno.com", function(error, remainingCookies) { + if(error) { + console.error("Error checking remaining cookies:", error) + throw new Error(`Failed to get remaining cookies: ${error.message || error}`) + } + + test("should have no cookies remaining after deletion", function() { + expect(remainingCookies).to.be.an('array'); + expect(remainingCookies.length).to.equal(0); + console.log("✓ Confirmed: no cookies remain for domain after deleteCookies"); + }); + }); + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/folder.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/folder.bru new file mode 100644 index 000000000..1ae98288d --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/folder.bru @@ -0,0 +1,8 @@ +meta { + name: cookies + seq: 17 +} + +auth { + mode: inherit +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookie.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookie.bru new file mode 100644 index 000000000..729592345 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookie.bru @@ -0,0 +1,38 @@ +meta { + name: getCookie + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +tests { + const jar = bru.cookies.jar() + + jar.getCookie("https://testbench-sanity.usebruno.com", "__cf_bm", function(error, data) { + if(error) { + console.error("Cookie retrieval error:", error) + throw new Error(`Failed to get cookie: ${error.message || error}`) + } + + test("should successfully retrieve cookie data", function() { + expect(data).to.have.property('key'); + expect(data).to.have.property('value'); + expect(data.key).to.equal("__cf_bm"); + expect(data.value).to.be.a('string'); + expect(data.value).to.not.be.empty; + expect(data.domain).to.include('usebruno.com'); + console.log("Retrieved cookie:", data); + }); + }) + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookies.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookies.bru new file mode 100644 index 000000000..7c09371c7 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/getCookies.bru @@ -0,0 +1,52 @@ +meta { + name: getCookies + type: http + seq: 3 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +tests { + const jar = bru.cookies.jar() + + jar.getCookies("https://testbench-sanity.usebruno.com", function(error, data) { + if(error) { + console.error("Cookies retrieval error:", error) + throw new Error(`Failed to get cookies: ${error.message || error}`) + } + + test("should successfully retrieve cookies array", function() { + expect(error).to.be.null; + expect(data).to.not.be.null; + expect(data).to.be.an('array'); + console.log("Retrieved cookies count:", data.length); + }); + + test("should have valid cookie structure in array", function() { + data.forEach((cookie, index) => { + expect(cookie).to.have.property('key'); + expect(cookie).to.have.property('value'); + expect(cookie.key).to.be.a('string'); + expect(cookie.value).to.be.a('string'); + expect(cookie.domain).to.include('usebruno.com'); + console.log(`Cookie ${index + 1}:`, cookie); + }); + }); + + test("should contain expected cookie properties", function() { + const cookieKeys = data.map(cookie => cookie.key); + expect(cookieKeys).to.be.an('array'); + console.log("Found cookie keys:", cookieKeys); + }); + }) + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookie.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookie.bru new file mode 100644 index 000000000..5449a248a --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookie.bru @@ -0,0 +1,69 @@ +meta { + name: setCookie + type: http + seq: 2 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + // Set cookie before the request + try { + await jar.setCookie("https://testbench-sanity.usebruno.com", { + key: "auth", + value: "1234", + path: "/path" + }); + + console.log("Cookie set successfully in pre-request script"); + + } catch (error) { + console.error("Cookie setting error in pre-request:", error); + throw new Error(`Pre-request setCookie failed: ${error.message || error}`); + } +} + +tests { + const jar = bru.cookies.jar() + + test("should have set cookie successfully", function() { + console.log("Verifying cookie set in pre-request script"); + }); + + // Test: Verify the cookie was set by retrieving it + const cookieData = await jar.getCookie("https://testbench-sanity.usebruno.com/path", "auth"); + + test("should retrieve the set cookie with correct properties", function() { + expect(cookieData.key).to.equal("auth"); + expect(cookieData.value).to.equal("1234"); + expect(cookieData.path).to.equal("/path"); + expect(cookieData.domain).to.include('usebruno.com'); + console.log("Retrieved and verified cookie:", cookieData); + }); + + // Test: Additional verification - check all cookies for the domain + const allCookies = await jar.getCookies("https://testbench-sanity.usebruno.com/path"); + + test("should find the cookie in domain cookie list", function() { + expect(allCookies).to.be.an('array'); + expect(allCookies.length).to.be.at.least(1); + + const authCookie = allCookies.find(c => c.key === 'auth'); + expect(authCookie).to.not.be.undefined; + expect(authCookie.value).to.equal("1234"); + + console.log("All cookies for domain:", allCookies.map(c => ({ key: c.key, value: c.value, path: c.path }))); + }); + + jar.clear() +} + +settings { + encodeUrl: true +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookieHeader.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookieHeader.bru new file mode 100644 index 000000000..c388917ba --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookieHeader.bru @@ -0,0 +1,40 @@ +meta { + name: setCookie header inclusion + type: http + seq: 6 +} + +post { + url: {{echo-host}} + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar(); + + // Set a cookie that should be sent with the upcoming request + await jar.setCookie('https://echo.usebruno.com', { + key: 'auth', + value: 'token123', + path: '/', + secure: false + }); +} + +tests { + const cookieHeader = res.getHeader('cookie'); + + test('should attach auth cookie in request headers', function () { + expect(cookieHeader).to.be.a('string'); + expect(cookieHeader).to.include('auth=token123'); + }); + + // Clean up the jar so other tests are not affected + const jar = bru.cookies.jar(); + await jar.clear(); +} + +settings { + encodeUrl: false +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookies.bru b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookies.bru new file mode 100644 index 000000000..87cefde76 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/cookies/setCookies.bru @@ -0,0 +1,85 @@ +meta { + name: setCookies + type: http + seq: 4 +} + +get { + url: {{host}}/ping + body: none + auth: inherit +} + +script:pre-request { + const jar = bru.cookies.jar() + + // Set multiple cookies before the request + try { + await jar.setCookies('https://example.com', [ + { + key: 'auth', + value: 'abc123', + path: '/path', + httpOnly: true, + secure: true, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000) + }, + { + key: 'session', + value: 'xyz789', + path: '/foo', + httpOnly: true, + secure: true, + } + ]); + + console.log("Multiple cookies set successfully in pre-request script"); + + } catch (error) { + console.error("setCookies operation failed in pre-request:", error); + throw new Error(`Pre-request setCookies failed: ${error.message || error}`); + } +} + +tests { + const jar = bru.cookies.jar() + + test("should have set multiple cookies successfully", function() { + console.log("Verifying cookies set in pre-request script"); + }); + + // Test: Verify first cookie was set correctly + const authCookie = await jar.getCookie('https://example.com/path', 'auth'); + + test("should retrieve first cookie with correct properties", function() { + expect(authCookie.key).to.equal("auth"); + expect(authCookie.value).to.equal("abc123"); + expect(authCookie.path).to.equal("/path"); + expect(authCookie.httpOnly).to.be.true; + expect(authCookie.secure).to.be.true; + expect(authCookie.domain).to.include('example.com'); + console.log("Auth cookie verified:", authCookie); + }); + + // Test: Verify second cookie was set correctly + const sessionCookie = await jar.getCookie('https://example.com/foo', 'session'); + + test("should retrieve second cookie with correct properties", function() { + expect(sessionCookie).to.not.be.null; + if (sessionCookie) { + expect(sessionCookie.key).to.equal("session"); + expect(sessionCookie.value).to.equal("xyz789"); + expect(sessionCookie.path).to.equal("/foo"); + expect(sessionCookie.httpOnly).to.be.true; + expect(sessionCookie.secure).to.be.true; + expect(sessionCookie.domain).to.include('example.com'); + console.log("Session cookie verified:", sessionCookie); + } + }); + + jar.clear() +} + +settings { + encodeUrl: true +} From ec51ebba451feb0720b8626816f7f67de04daac3 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Thu, 31 Jul 2025 00:00:23 +0530 Subject: [PATCH 14/30] Add Select/Deselect and Reorder Capabilities to Collection Runner (#5195) --- .../RunConfigurationPanel/StyledWrapper.jsx | 231 +++++++++++ .../RunConfigurationPanel/index.jsx | 327 ++++++++++++++++ .../RunnerResults/RunnerTags/index.jsx | 6 +- .../src/components/RunnerResults/index.jsx | 359 +++++++++++------- .../ReduxStore/slices/collections/actions.js | 35 +- .../ReduxStore/slices/collections/index.js | 12 + .../bruno-electron/src/ipc/network/index.js | 4 +- packages/bruno-filestore/src/index.ts | 46 ++- 8 files changed, 854 insertions(+), 166 deletions(-) create mode 100644 packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/StyledWrapper.jsx create mode 100644 packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx diff --git a/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/StyledWrapper.jsx b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/StyledWrapper.jsx new file mode 100644 index 000000000..0a6dbeea2 --- /dev/null +++ b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/StyledWrapper.jsx @@ -0,0 +1,231 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + background-color: ${props => props.theme.sidebar.bg}; + height: 100%; + display: flex; + flex-direction: column; + width: 100%; + overflow: hidden; + + .header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + border-bottom: 1px solid ${props => props.theme.sidebar.dragbar}; + margin-bottom: 0.5rem; + + .counter { + font-size: 0.875rem; + font-weight: 500; + } + + .actions { + display: flex; + align-items: center; + gap: 0.75rem; + } + + .btn-select-all, + .btn-reset { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.75rem; + color: ${props => props.theme.textLink}; + background: none; + border: none; + padding: 0.25rem 0.5rem; + cursor: pointer; + + &:hover { + text-decoration: underline; + } + } + } + + .request-list { + flex: 1; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: ${props => props.theme.console.scrollbarThumb}; + border-radius: 3px; + } + + .loading-message, + .empty-message { + padding: 0.75rem; + color: ${props => props.theme.colors.text.muted}; + font-size: 0.875rem; + } + + .requests-container { + padding: 0.5rem; + position: relative; + } + } + + .request-item { + display: flex; + align-items: center; + padding: 0.5rem; + border-radius: 4px; + margin-bottom: 0.25rem; + position: relative; + height: 2.5rem; + border: 1px solid transparent; + background-color: ${props => props.theme.sidebar.bg}; + transition: transform 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; + + &.is-selected { + background-color: ${props => props.theme.requestTabs.active.bg}; + } + + &.is-dragging { + opacity: 0.5; + background-color: ${props => props.theme.sidebar.bg}; + border: 1px dashed ${props => props.theme.sidebar.dragbar}; + transform: scale(0.98); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12); + z-index: 5; + } + + &::before, + &::after { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 2px; + background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink}; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; + } + + &::before { + top: -1px; + } + + &::after { + bottom: -1px; + } + + &.drop-target-above { + &::before { + opacity: 1; + height: 2px; + background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink}; + } + } + + &.drop-target-below { + &::after { + opacity: 1; + height: 2px; + background: ${props => props.theme.dragAndDrop?.border || props.theme.textLink}; + } + } + + .drag-handle { + cursor: grab; + margin-right: 0.25rem; + color: ${props => props.theme.sidebar.muted}; + display: flex; + align-items: center; + transition: color 0.15s ease; + + &:hover { + color: ${props => props.theme.text}; + } + + &:active { + cursor: grabbing; + color: ${props => props.theme.textLink}; + } + } + + .checkbox-container { + cursor: pointer; + margin-right: 0.5rem; + + .checkbox { + width: 1rem; + height: 1rem; + border: 1px solid ${props => props.theme.sidebar.dragbar}; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.1s ease; + + &:hover { + border-color: ${props => props.theme.textLink}; + } + } + } + + .method { + font-family: monospace; + font-size: 0.75rem; + font-weight: 500; + margin-right: 0.5rem; + min-width: 3rem; + color: ${props => props.theme.sidebar.muted}; // Default color for unknown methods + + &.method-get { + color: ${props => props.theme.request.methods.get}; + } + + &.method-post { + color: ${props => props.theme.request.methods.post}; + } + + &.method-put { + color: ${props => props.theme.request.methods.put}; + } + + &.method-delete { + color: ${props => props.theme.request.methods.delete}; + } + + &.method-patch { + color: ${props => props.theme.request.methods.patch}; + } + + &.method-options { + color: ${props => props.theme.request.methods.options}; + } + + &.method-head { + color: ${props => props.theme.request.methods.head}; + } + } + + .request-name { + flex: 1; + font-size: 0.875rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + .folder-path { + margin-left: 0.5rem; + font-size: 0.75rem; + color: ${props => props.theme.sidebar.muted}; + } + } + } +`; + +export default StyledWrapper; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx new file mode 100644 index 000000000..3f137cad9 --- /dev/null +++ b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx @@ -0,0 +1,327 @@ +import React, { useEffect, useState, useCallback, useRef } from 'react'; +import { useDrag, useDrop } from 'react-dnd'; +import { getEmptyImage } from 'react-dnd-html5-backend'; +import { IconGripVertical, IconCheck, IconAdjustmentsAlt } from '@tabler/icons'; +import { useDispatch } from 'react-redux'; +import { updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; +import { isItemARequest } from 'utils/collections'; +import path from 'utils/common/path'; +import { cloneDeep, get } from 'lodash'; + +const ItemTypes = { + REQUEST_ITEM: 'request-item' +}; + +const RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop }) => { + const ref = useRef(null); + const [dropType, setDropType] = useState(null); + + const determineDropType = (monitor) => { + const hoverBoundingRect = ref.current?.getBoundingClientRect(); + const clientOffset = monitor.getClientOffset(); + if (!hoverBoundingRect || !clientOffset) return null; + + const clientY = clientOffset.y - hoverBoundingRect.top; + const middleY = hoverBoundingRect.height / 2; + + return clientY < middleY ? 'above' : 'below'; + }; + + const [{ isDragging }, drag, preview] = useDrag({ + type: ItemTypes.REQUEST_ITEM, + item: { uid: item.uid, name: item.name, request: item.request, index }, + collect: (monitor) => ({ isDragging: monitor.isDragging() }), + options: { + dropEffect: "move" + }, + end: (draggedItem, monitor) => { + if (monitor.didDrop()) { + onDrop(); + } + }, + }); + + const [{ isOver, canDrop }, drop] = useDrop({ + accept: ItemTypes.REQUEST_ITEM, + hover: (draggedItem, monitor) => { + if (draggedItem.uid === item.uid) { + setDropType(null); + return; + } + + const dropType = determineDropType(monitor); + setDropType(dropType); + }, + drop: (draggedItem, monitor) => { + if (draggedItem.uid === item.uid) return; + + const dropType = determineDropType(monitor); + let targetIndex = index; + + if (dropType === 'below') { + targetIndex = index + 1; + } + + if (draggedItem.index < targetIndex) { + targetIndex = targetIndex - 1; + } + + moveItem(draggedItem.uid, targetIndex); + setDropType(null); + return { item: draggedItem }; + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop() + }), + }); + + useEffect(() => { + preview(getEmptyImage(), { captureDraggingState: true }); + }, []); + + // Clear drop type when not hovering + useEffect(() => { + if (!isOver) { + setDropType(null); + } + }, [isOver]); + + drag(drop(ref)); + + const itemClasses = [ + 'request-item', + isDragging ? 'is-dragging' : '', + isSelected ? 'is-selected' : '', + isOver && canDrop && dropType === 'above' ? 'drop-target-above' : '', + isOver && canDrop && dropType === 'below' ? 'drop-target-below' : '' + ].filter(Boolean).join(' '); + + return ( +
+
+ +
+ +
onSelect(item)}> +
+ {isSelected && } +
+
+ +
+ {item.request?.method.toUpperCase()} +
+ +
+ {item.name} + {item.folderPath && ( + {item.folderPath} + )} +
+
+ ); +}; + +const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) => { + const dispatch = useDispatch(); + const [flattenedRequests, setFlattenedRequests] = useState([]); + const [originalRequests, setOriginalRequests] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const flattenRequests = useCallback((collection) => { + const result = []; + + const processItems = (items) => { + if (!items?.length) return; + + items.forEach(item => { + if (isItemARequest(item) && !item.partial) { + const relativePath = path.relative(collection.pathname, path.dirname(item.pathname)); + const folderPath = relativePath !== '.' ? relativePath : ''; + + result.push({ + ...item, + folderPath: folderPath.replace(/\\/g, '/') + }); + } + + if (item.items?.length) { + processItems(item.items); + } + }); + }; + + processItems(collection.items); + return result; + }, []); + + useEffect(() => { + setIsLoading(true); + + try { + const structureCopy = cloneDeep(collection); + const requests = flattenRequests(structureCopy); + + const savedConfiguration = get(collection, 'runnerConfiguration', null); + if (savedConfiguration?.requestItemsOrder?.length > 0) { + const orderedRequests = []; + const requestMap = new Map(requests.map(req => [req.uid, req])); + + savedConfiguration.requestItemsOrder.forEach(uid => { + const request = requestMap.get(uid); + if (request) { + orderedRequests.push(request); + requestMap.delete(uid); + } + }); + + requestMap.forEach(request => { + orderedRequests.push(request); + }); + + setFlattenedRequests(orderedRequests); + } else { + setFlattenedRequests(requests); + } + + setOriginalRequests(cloneDeep(requests)); + } catch (error) { + console.error("Error loading collection structure:", error); + } finally { + setIsLoading(false); + } + }, [collection, flattenRequests]); + + const moveItem = useCallback((draggedItemUid, hoverIndex) => { + setFlattenedRequests((prevRequests) => { + const dragIndex = prevRequests.findIndex(item => item.uid === draggedItemUid); + + if (dragIndex === -1 || dragIndex === hoverIndex) { + return prevRequests; + } + + const updatedRequests = [...prevRequests]; + const [draggedItem] = updatedRequests.splice(dragIndex, 1); + updatedRequests.splice(hoverIndex, 0, draggedItem); + + return updatedRequests; + }); + }, []); + + const handleDrop = useCallback(() => { + const selectedUids = new Set(selectedItems); + + setFlattenedRequests(currentRequests => { + const newOrderedSelectedUids = currentRequests + .filter(item => selectedUids.has(item.uid)) + .map(item => item.uid); + + const allRequestUidsOrder = currentRequests.map(item => item.uid); + + setSelectedItems(newOrderedSelectedUids); + dispatch(updateRunnerConfiguration(collection.uid, newOrderedSelectedUids, allRequestUidsOrder)); + + return currentRequests; + }); + }, [selectedItems, collection.uid, dispatch, setSelectedItems]); + + const handleRequestSelect = useCallback((item) => { + try { + if (selectedItems.includes(item.uid)) { + const newSelectedUids = selectedItems.filter(uid => uid !== item.uid); + setSelectedItems(newSelectedUids); + + const allRequestUidsOrder = flattenedRequests.map(item => item.uid); + dispatch(updateRunnerConfiguration(collection.uid, newSelectedUids, allRequestUidsOrder)); + } else { + const newSelectedUids = [...selectedItems, item.uid]; + + const orderedSelectedUids = flattenedRequests + .filter(req => newSelectedUids.includes(req.uid)) + .map(req => req.uid); + + setSelectedItems(orderedSelectedUids); + + const allRequestUidsOrder = flattenedRequests.map(item => item.uid); + dispatch(updateRunnerConfiguration(collection.uid, orderedSelectedUids, allRequestUidsOrder)); + } + } catch (error) { + console.error("Error selecting item:", error); + } + }, [selectedItems, setSelectedItems, flattenedRequests, dispatch, collection.uid]); + + const handleSelectAll = useCallback(() => { + try { + if (selectedItems.length === flattenedRequests.length) { + setSelectedItems([]); + dispatch(updateRunnerConfiguration(collection.uid, [], [])); + } else { + const allUids = flattenedRequests.map(item => item.uid); + setSelectedItems(allUids); + const allRequestUidsOrder = flattenedRequests.map(item => item.uid); + dispatch(updateRunnerConfiguration(collection.uid, allUids, allRequestUidsOrder)); + } + } catch (error) { + console.error("Error selecting/deselecting all items:", error); + } + }, [flattenedRequests, selectedItems, setSelectedItems, dispatch, collection.uid]); + + const handleReset = useCallback(() => { + try { + setFlattenedRequests(cloneDeep(originalRequests)); + setSelectedItems([]); + dispatch(updateRunnerConfiguration(collection.uid, [], [])); + } catch (error) { + console.error("Error resetting configuration:", error); + } + }, [originalRequests, setSelectedItems, collection.uid, dispatch]); + + return ( + +
+
+ {selectedItems.length} of {flattenedRequests.length} selected +
+
+ + +
+
+ +
+ {isLoading ? ( +
Loading requests...
+ ) : flattenedRequests.length === 0 ? ( +
No requests found in this collection
+ ) : ( +
+ {flattenedRequests.map((item, idx) => { + const isSelected = selectedItems.includes(item.uid); + + return ( + handleRequestSelect(item)} + moveItem={moveItem} + onDrop={handleDrop} + /> + ); + })} +
+ )} +
+
+ ); +}; + +export default RunConfigurationPanel; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RunnerResults/RunnerTags/index.jsx b/packages/bruno-app/src/components/RunnerResults/RunnerTags/index.jsx index 984452d78..b61b5b9f6 100644 --- a/packages/bruno-app/src/components/RunnerResults/RunnerTags/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/RunnerTags/index.jsx @@ -89,13 +89,15 @@ const RunnerTags = ({ collectionUid, className = '' }) => { return (
- setTagsEnabled(!tagsEnabled)} /> +
{tagsEnabled && (
diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index a5dc83a1a..897d9d241 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react'; import path from 'utils/common/path'; import { useDispatch } from 'react-redux'; import { get, cloneDeep } from 'lodash'; -import { runCollectionFolder, cancelRunnerExecution, mountCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions'; import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections'; import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections'; import { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconLoader2 } from '@tabler/icons'; @@ -10,7 +10,9 @@ import ResponsePane from './ResponsePane'; import StyledWrapper from './StyledWrapper'; import { areItemsLoading } from 'utils/collections'; import RunnerTags from './RunnerTags/index'; +import RunConfigurationPanel from './RunConfigurationPanel'; import { getRequestItemsForCollectionRun } from 'utils/collections/index'; +import { updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections/index'; const getDisplayName = (fullPath, pathname, name = '') => { let relativePath = path.relative(fullPath, pathname); @@ -25,25 +27,27 @@ const getTestStatus = (results) => { }; const allTestsPassed = (item) => { - return item.status !== 'error' && - item.testStatus === 'pass' && - item.assertionStatus === 'pass' && - item.preRequestTestStatus === 'pass' && - item.postResponseTestStatus === 'pass'; + return item.status !== 'error' && + item.testStatus === 'pass' && + item.assertionStatus === 'pass' && + item.preRequestTestStatus === 'pass' && + item.postResponseTestStatus === 'pass'; }; const anyTestFailed = (item) => { - return item.status === 'error' || - item.testStatus === 'fail' || - item.assertionStatus === 'fail' || - item.preRequestTestStatus === 'fail' || - item.postResponseTestStatus === 'fail'; + return item.status === 'error' || + item.testStatus === 'fail' || + item.assertionStatus === 'fail' || + item.preRequestTestStatus === 'fail' || + item.postResponseTestStatus === 'fail'; }; export default function RunnerResults({ collection }) { const dispatch = useDispatch(); const [selectedItem, setSelectedItem] = useState(null); const [delay, setDelay] = useState(null); + const [selectedRequestItems, setSelectedRequestItems] = useState([]); + const [configureMode, setConfigureMode] = useState(false); // ref for the runner output body const runnerBodyRef = useRef(); @@ -62,6 +66,22 @@ export default function RunnerResults({ collection }) { autoScrollRunnerBody(); }, [collection, setSelectedItem]); + useEffect(() => { + const runnerInfo = get(collection, 'runnerResult.info', {}); + if (runnerInfo.status === 'running') { + setConfigureMode(false); + } + }, [collection.runnerResult]); + + useEffect(() => { + const savedConfiguration = get(collection, 'runnerConfiguration', null); + if (savedConfiguration && configureMode) { + if (savedConfiguration.selectedRequestItems) { + setSelectedRequestItems(savedConfiguration.selectedRequestItems); + } + } + }, [collection.runnerConfiguration, configureMode]); + const collectionCopy = cloneDeep(collection); const runnerInfo = get(collection, 'runnerResult.info', {}); @@ -115,19 +135,27 @@ export default function RunnerResults({ collection }) { }; const runCollection = () => { - ensureCollectionIsMounted(); - dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags)); + if (configureMode && selectedRequestItems.length > 0) { + dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, selectedRequestItems)); + dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags, selectedRequestItems)); + } else { + dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags)); + } }; const runAgain = () => { ensureCollectionIsMounted(); + // Get the saved configuration to determine what to run + const savedConfiguration = get(collection, 'runnerConfiguration', null); + const savedSelectedItems = savedConfiguration?.selectedRequestItems || []; dispatch( runCollectionFolder( collection.uid, runnerInfo.folderUid, - runnerInfo.isRecursive, + true, Number(delay), - tagsEnabled && tags + tagsEnabled && tags, + savedSelectedItems ) ); }; @@ -138,12 +166,25 @@ export default function RunnerResults({ collection }) { collectionUid: collection.uid }) ); + setSelectedRequestItems([]); + setConfigureMode(false); }; const cancelExecution = () => { dispatch(cancelRunnerExecution(runnerInfo.cancelTokenUid)); }; + const toggleConfigureMode = () => { + dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled: false })); + setConfigureMode(!configureMode); + }; + + useEffect(() => { + if(tagsEnabled) { + setConfigureMode(false); + } + }, [tagsEnabled]); + const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy); const passedRequests = items.filter(allTestsPassed); const failedRequests = items.filter(anyTestFailed); @@ -155,66 +196,104 @@ export default function RunnerResults({ collection }) { if (!items || !items.length) { return ( - -
- Runner - -
-
- You have {totalRequestsInCollection} requests in this collection. - {isCollectionLoading && ( - - (Loading...) - + +
+
+
+ Runner + +
+
+ You have {totalRequestsInCollection} requests in this collection. + {isCollectionLoading && ( + + (Loading...) + + )} +
+ {isCollectionLoading ?
Requests in this collection are still loading.
: null} +
+ + setDelay(e.target.value)} + /> +
+ + {/* Tags for the collection run */} + + + {/* Configure requests option */} +
+
+ + +
+
+ +
+ + + +
+
+ + {configureMode && ( +
+ +
)}
-
- - setDelay(e.target.value)} - /> -
- - {/* Tags for the collection run */} - - -
- - - -
); } return ( -
-
+
+
Runner
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && ( - )}
-
+ +
@@ -234,57 +313,59 @@ export default function RunnerResults({ collection }) {
)} - {runnerInfo?.statusText ? + {runnerInfo?.statusText ?
{runnerInfo?.statusText}
- : null} - - {items.map((item) => { - return ( -
-
-
- - {allTestsPassed(item) ? - - : null} - {item.status === 'skipped' ? - - :null} - {anyTestFailed(item) ? - - :null} - - - {item.displayName} - - {item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? ( - - ) : item.responseReceived?.status ? ( - setSelectedItem(item)}> - {item.responseReceived?.status} - -  - {item.responseReceived?.statusText} - - ) : ( - setSelectedItem(item)}> - (request failed) - - )} -
- {tagsEnabled && areTagsAdded && item?.tags?.length > 0 && ( -
- Tags: {item.tags.filter(t => tags.include.includes(t)).join(', ')} -
- )} - {item.status == 'error' ?
{item.error}
: null} + : null} -
    - {item.preRequestTestResults - ? item.preRequestTestResults.map((result) => ( + {/* Items list */} +
    + {items.map((item) => { + return ( +
    +
    +
    + + {allTestsPassed(item) ? + + : null} + {item.status === 'skipped' ? + + : null} + {anyTestFailed(item) ? + + : null} + + + {item.displayName} + + {item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? ( + + ) : item.responseReceived?.status ? ( + setSelectedItem(item)}> + {item.responseReceived?.status} + -  + {item.responseReceived?.statusText} + + ) : ( + setSelectedItem(item)}> + (request failed) + + )} +
    + {tagsEnabled && areTagsAdded && item?.tags?.length > 0 && ( +
    + Tags: {item.tags.filter(t => tags.include.includes(t)).join(', ')} +
    + )} + {item.status == 'error' ?
    {item.error}
    : null} + +
      + {item.preRequestTestResults + ? item.preRequestTestResults.map((result) => (
    • {result.status === 'pass' ? ( @@ -302,9 +383,9 @@ export default function RunnerResults({ collection }) { )}
    • )) - : null} - {item.postResponseTestResults - ? item.postResponseTestResults.map((result) => ( + : null} + {item.postResponseTestResults + ? item.postResponseTestResults.map((result) => (
    • {result.status === 'pass' ? ( @@ -322,9 +403,9 @@ export default function RunnerResults({ collection }) { )}
    • )) - : null} - {item.testResults - ? item.testResults.map((result) => ( + : null} + {item.testResults + ? item.testResults.map((result) => (
    • {result.status === 'pass' ? ( @@ -342,30 +423,32 @@ export default function RunnerResults({ collection }) { )}
    • )) - : null} - {item.assertionResults?.map((result) => ( -
    • - {result.status === 'pass' ? ( - - - {result.lhsExpr}: {result.rhsExpr} - - ) : ( - <> - - + : null} + {item.assertionResults?.map((result) => ( +
    • + {result.status === 'pass' ? ( + + {result.lhsExpr}: {result.rhsExpr} - {result.error} - - )} -
    • - ))} -
    + ) : ( + <> + + + {result.lhsExpr}: {result.rhsExpr} + + {result.error} + + )} + + ))} +
+
-
- ); - })} + ); + })} +
+ {runnerInfo.status === 'ended' ? (
-
+
{ collection={collection} isSecret={true} /> + {showWarning && }
diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js index 9ea532646..d0cf9d722 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -12,6 +14,8 @@ const BasicAuth = ({ collection }) => { const { storedTheme } = useTheme(); const basicAuth = get(collection, 'root.request.auth.basic', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(basicAuth?.password); const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); @@ -55,7 +59,7 @@ const BasicAuth = ({ collection }) => {
-
+
{ collection={collection} isSecret={true} /> + {showWarning && }
); diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js index 82f8be12c..788182479 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -12,6 +14,8 @@ const BearerAuth = ({ collection }) => { const { storedTheme } = useTheme(); const bearerToken = get(collection, 'root.request.auth.bearer.token', ''); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(bearerToken); const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); @@ -30,7 +34,7 @@ const BearerAuth = ({ collection }) => { return ( -
+
{ collection={collection} isSecret={true} /> + {showWarning && }
); diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js index 582b17b82..22981f56b 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -12,6 +14,8 @@ const DigestAuth = ({ collection }) => { const { storedTheme } = useTheme(); const digestAuth = get(collection, 'root.request.auth.digest', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(digestAuth?.password); const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); @@ -55,7 +59,7 @@ const DigestAuth = ({ collection }) => {
-
+
{ collection={collection} isSecret={true} /> + {showWarning && }
); diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js index 173c99a12..38a9c18f0 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -18,6 +20,8 @@ const NTLMAuth = ({ collection }) => { const { storedTheme } = useTheme(); const ntlmAuth = get(collection, 'root.request.auth.ntlm', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password); const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); @@ -82,7 +86,7 @@ const NTLMAuth = ({ collection }) => {
-
+
{ collection={collection} isSecret={true} /> + {showWarning && }
diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js index 2e1a2c65c..226cedd7b 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js @@ -1,4 +1,6 @@ import React from 'react'; +import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; +import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField'; import get from 'lodash/get'; import { useTheme } from 'providers/Theme'; import { useDispatch } from 'react-redux'; @@ -12,6 +14,8 @@ const WsseAuth = ({ collection }) => { const { storedTheme } = useTheme(); const wsseAuth = get(collection, 'root.request.auth.wsse', {}); + const { isSensitive } = useDetectSensitiveField(collection); + const { showWarning, warningMessage } = isSensitive(wsseAuth?.password); const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); @@ -55,14 +59,16 @@ const WsseAuth = ({ collection }) => {
-
+
handlePasswordChange(val)} collection={collection} + isSecret={true} /> + {showWarning && }
); diff --git a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js index 27cab21ce..5ba3b0797 100644 --- a/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js +++ b/packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js @@ -1,5 +1,6 @@ import React, { useRef, useEffect, useMemo } from 'react'; import cloneDeep from 'lodash/cloneDeep'; +import { get } from 'lodash'; import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons'; import { useTheme } from 'providers/Theme'; import { useDispatch, useSelector } from 'react-redux'; @@ -14,8 +15,7 @@ import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions import toast from 'react-hot-toast'; import { Tooltip } from 'react-tooltip'; import SensitiveFieldWarning from 'components/SensitiveFieldWarning'; -import { getGlobalEnvironmentVariables, flattenItems } from 'utils/collections'; -import { isItemARequest } from 'utils/collections'; +import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'utils/collections'; import { sensitiveFields } from './constants'; const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => { @@ -39,19 +39,35 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original return result; } const varNames = new Set(nonSecretVars.map((v) => v.name)); + + const checkSensitiveField = (obj, fieldPath) => { + const value = get(obj, fieldPath); + if (typeof value === 'string') { + varNames.forEach((varName) => { + if (new RegExp(`\{\{\s*${varName}\s*\}\}`).test(value)) { + result[varName] = true; + } + }); + } + }; + + const getObjectToProcess = (item) => { + if (isItemARequest(item)) { + return item.draft || item; + } + return item.root; + }; + + const collectionObj = getObjectToProcess(collection); + sensitiveFields.forEach((fieldPath) => { + checkSensitiveField(collectionObj, fieldPath); + }); + const items = flattenItems(collection.items || []); items.forEach((item) => { - if (!isItemARequest(item)) return; - const requestObj = item.draft ? item.draft : item; + const objToProcess = getObjectToProcess(item); sensitiveFields.forEach((fieldPath) => { - const value = fieldPath.split('.').reduce((obj, key) => (obj ? obj[key] : undefined), requestObj); - if (typeof value === 'string') { - varNames.forEach((varName) => { - if (new RegExp(`\{\{\s*${varName}\s*\}\}`).test(value)) { - result[varName] = true; - } - }); - } + checkSensitiveField(objToProcess, fieldPath); }); }); return result; From e2ecd7bfa9a8bf3e204c13edc3f7a913b234bf94 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Fri, 1 Aug 2025 20:59:37 +0530 Subject: [PATCH 18/30] fix: request tab opening unintentionally (#5240) --- .../middlewares/tasks/middleware.js | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js index 056136a1c..ad106cde5 100644 --- a/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js +++ b/packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js @@ -27,22 +27,25 @@ taskMiddleware.startListening({ each(openRequestTasks, (task) => { if (collectionUid === task.collectionUid) { const collection = findCollectionByUid(state.collections.collections, collectionUid); - const item = findItemInCollectionByPathname(collection, task.itemPathname); - if (item) { - listenerApi.dispatch( - addTab({ - uid: item.uid, - collectionUid: collection.uid, - requestPaneTab: getDefaultRequestPaneTab(item) - }) - ); - listenerApi.dispatch(hideHomePage()); - listenerApi.dispatch( - removeTaskFromQueue({ - taskUid: task.uid - }) - ); + if (collection && collection.mountStatus === 'mounted' && !collection.isLoading) { + const item = findItemInCollectionByPathname(collection, task.itemPathname); + if (item) { + listenerApi.dispatch( + addTab({ + uid: item.uid, + collectionUid: collection.uid, + requestPaneTab: getDefaultRequestPaneTab(item) + }) + ); + listenerApi.dispatch(hideHomePage()); + } } + + listenerApi.dispatch( + removeTaskFromQueue({ + taskUid: task.uid + }) + ); } }); } From 110d93a9836e1c6f666c1bf6aa19781e852290fd Mon Sep 17 00:00:00 2001 From: lohit Date: Fri, 1 Aug 2025 20:59:57 +0530 Subject: [PATCH 19/30] global environments fetch error handling (#5241) --- packages/bruno-electron/src/ipc/preferences.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/bruno-electron/src/ipc/preferences.js b/packages/bruno-electron/src/ipc/preferences.js index 4c9c34d99..3cf95c6f4 100644 --- a/packages/bruno-electron/src/ipc/preferences.js +++ b/packages/bruno-electron/src/ipc/preferences.js @@ -15,11 +15,17 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => { const { http_proxy, https_proxy, no_proxy } = systemProxyVars || {}; mainWindow.webContents.send('main:load-system-proxy-env', { http_proxy, https_proxy, no_proxy }); - // load global environments - const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments(); - let activeGlobalEnvironmentUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUid(); - activeGlobalEnvironmentUid = globalEnvironments?.find(env => env?.uid == activeGlobalEnvironmentUid) ? activeGlobalEnvironmentUid : null; - mainWindow.webContents.send('main:load-global-environments', { globalEnvironments, activeGlobalEnvironmentUid }); + try { + // load global environments + const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments(); + let activeGlobalEnvironmentUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUid(); + activeGlobalEnvironmentUid = globalEnvironments?.find(env => env?.uid == activeGlobalEnvironmentUid) ? activeGlobalEnvironmentUid : null; + mainWindow.webContents.send('main:load-global-environments', { globalEnvironments, activeGlobalEnvironmentUid }); + } + catch(error) { + console.error("Error occured while fetching global environements!"); + console.error(error); + } // reload last opened collections const lastOpened = lastOpenedCollections.getAll(); From 0e81c14b96974dbc1513d594219ad56dae7afff5 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <161328623+sanjaikumar-bruno@users.noreply.github.com> Date: Fri, 1 Aug 2025 21:00:19 +0530 Subject: [PATCH 20/30] fix: correct password field binding in DigestAuth component (#5242) Co-authored-by: sanjai0py --- .../src/components/RequestPane/Auth/DigestAuth/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js index d17126c37..e872894d8 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js @@ -68,7 +68,7 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
handlePasswordChange(val)} From 99c8fd524086a3046e5cad45113475099eec65e9 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Tue, 5 Aug 2025 17:11:49 +0530 Subject: [PATCH 21/30] fix: request order reset on select all (#5261) --- .../RunnerResults/RunConfigurationPanel/index.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx index 3f137cad9..aa3a3c9f0 100644 --- a/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx @@ -254,14 +254,14 @@ const RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) const handleSelectAll = useCallback(() => { try { + const allRequestUidsOrder = flattenedRequests.map(item => item.uid); + if (selectedItems.length === flattenedRequests.length) { setSelectedItems([]); - dispatch(updateRunnerConfiguration(collection.uid, [], [])); + dispatch(updateRunnerConfiguration(collection.uid, [], allRequestUidsOrder)); } else { - const allUids = flattenedRequests.map(item => item.uid); - setSelectedItems(allUids); - const allRequestUidsOrder = flattenedRequests.map(item => item.uid); - dispatch(updateRunnerConfiguration(collection.uid, allUids, allRequestUidsOrder)); + setSelectedItems(allRequestUidsOrder); + dispatch(updateRunnerConfiguration(collection.uid, allRequestUidsOrder, allRequestUidsOrder)); } } catch (error) { console.error("Error selecting/deselecting all items:", error); From 7cb80abdfc5f5cd38615cfdaa2f73c565dd00462 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Wed, 6 Aug 2025 16:22:30 +0530 Subject: [PATCH 22/30] fix: scrollbar visible in tables (#5270) --- packages/bruno-app/src/components/Table/StyledWrapper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/Table/StyledWrapper.js b/packages/bruno-app/src/components/Table/StyledWrapper.js index 74dc2e37a..5e7132c16 100644 --- a/packages/bruno-app/src/components/Table/StyledWrapper.js +++ b/packages/bruno-app/src/components/Table/StyledWrapper.js @@ -6,7 +6,7 @@ const StyledWrapper = styled.div` display: grid; overflow-y: hidden; overflow-x: auto; - padding: 0 1px; + padding: 0 1.5px; // for icon hover position: inherit; From 86901c1e89ecc9a0752b12a932b5b60761aa24be Mon Sep 17 00:00:00 2001 From: Pooja Date: Thu, 7 Aug 2025 15:50:03 +0530 Subject: [PATCH 23/30] fix: test only flag in cli to inclue pre and post test (#5216) --- packages/bruno-cli/src/commands/run.js | 16 +- packages/bruno-cli/src/utils/request.js | 44 +++ packages/bruno-cli/tests/utils/common.spec.js | 309 ++++++++++++++++++ 3 files changed, 365 insertions(+), 4 deletions(-) create mode 100644 packages/bruno-cli/src/utils/request.js create mode 100644 packages/bruno-cli/tests/utils/common.spec.js diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 1ca689fe2..e3b7aa3ea 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -14,6 +14,7 @@ const { getOptions } = require('../utils/bru'); const { parseDotEnv, parseEnvironment } = require('@usebruno/filestore'); const constants = require('../constants'); const { findItemInCollection, createCollectionJsonFromPathname, getCallStack } = require('../utils/collection'); +const { hasExecutableTestInScript } = require('../utils/request'); const command = 'run [paths...]'; const desc = 'Run one or more requests/folders'; @@ -467,10 +468,17 @@ const handler = async function (argv) { requestItems = getCallStack(resolvedPaths, collection, { recursive }); if (testsOnly) { - requestItems = requestItems.filter((iter) => { - const requestHasTests = iter.request?.tests; - const requestHasActiveAsserts = iter.request?.assertions.some((x) => x.enabled) || false; - return requestHasTests || requestHasActiveAsserts; + requestItems = requestItems.filter((item) => { + const requestHasTests = hasExecutableTestInScript(item.request?.tests); + const requestHasActiveAsserts = item.request?.assertions.some((x) => x.enabled) || false; + + const preRequestScript = item.request?.script?.req; + const requestHasPreRequestTests = hasExecutableTestInScript(preRequestScript); + + const postResponseScript = item.request?.script?.res; + const requestHasPostResponseTests = hasExecutableTestInScript(postResponseScript); + + return requestHasTests || requestHasActiveAsserts || requestHasPreRequestTests || requestHasPostResponseTests; }); } diff --git a/packages/bruno-cli/src/utils/request.js b/packages/bruno-cli/src/utils/request.js new file mode 100644 index 000000000..6a75a3cc7 --- /dev/null +++ b/packages/bruno-cli/src/utils/request.js @@ -0,0 +1,44 @@ + +// Check for meaningful test() calls (not commented out or in strings) +const hasExecutableTestInScript = (script) => { + if (!script) return false; + + // Remove single-line comments (// ...) and multi-line comments (/* ... */) + let cleanScript = script + .replace(/\/\/.*$/gm, '') // Remove line comments + .replace(/\/\*[\s\S]*?\*\//g, ''); // Remove block comments + + // Remove string literals to avoid matching test() inside strings + cleanScript = cleanScript + .replace(/"(?:[^"\\]|\\.)*"/g, '""') // Remove double-quoted strings + .replace(/'(?:[^'\\]|\\.)*'/g, "''") // Remove single-quoted strings + .replace(/`(?:[^`\\]|\\.)*`/g, '``'); // Remove template literals + + // Look for standalone test() calls (not object method calls like obj.test()) + // Find all test( occurrences and check they're not preceded by dots + let hasValidTest = false; + let searchFrom = 0; + + while (true) { + const index = cleanScript.indexOf('test', searchFrom); + if (index === -1) break; + + // Check if this looks like test( with optional whitespace + const afterTest = cleanScript.substring(index + 4); + if (/^\s*\(/.test(afterTest)) { + // Found test( - check if it's not preceded by a dot + if (index === 0 || cleanScript[index - 1] !== '.') { + hasValidTest = true; + break; + } + } + + searchFrom = index + 1; + } + + return hasValidTest; +}; + +module.exports = { + hasExecutableTestInScript +}; \ No newline at end of file diff --git a/packages/bruno-cli/tests/utils/common.spec.js b/packages/bruno-cli/tests/utils/common.spec.js new file mode 100644 index 000000000..bd4c1e7e0 --- /dev/null +++ b/packages/bruno-cli/tests/utils/common.spec.js @@ -0,0 +1,309 @@ +const { describe, it, expect } = require('@jest/globals'); +const { hasExecutableTestInScript } = require('../../src/utils/request'); + +describe('hasExecutableTestInScript', () => { + describe('should return true for valid test() calls', () => { + it('should detect basic test calls', () => { + const script = ` + test("should work", function() { + expect(true).to.be.true; + }); + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect indented test calls', () => { + const script = ` + if (true) { + test("indented test", function() { + expect(1).to.equal(1); + }); + } + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect test calls with extra whitespace', () => { + const script = `test ("with spaces", function() { });`; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect test calls after assignments', () => { + const script = ` + const result = test("assignment test", function() { + expect("hello").to.be.a("string"); + }); + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect test calls in conditionals', () => { + const script = ` + if (condition) { + test("conditional test", function() { + expect(true).to.be.true; + }); + } + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect test calls in arrays', () => { + const script = ` + const tests = [ + test("array test", function() { + expect(Array.isArray([])).to.be.true; + }) + ]; + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect test calls in ternary operators', () => { + const script = ` + const result = condition ? test("ternary test", function() { + expect(true).to.be.true; + }) : null; + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect test calls after semicolons', () => { + const script = ` + const data = res.data; test("after semicolon", function() { + expect(data).to.be.an("object"); + }); + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect test calls in object values', () => { + const script = ` + const config = { + validation: test("object value test", function() { + expect(true).to.be.true; + }) + }; + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect multiple test calls', () => { + const script = ` + test("first test", function() { + expect(1).to.equal(1); + }); + + test("second test", function() { + expect(2).to.equal(2); + }); + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should detect test calls at start of script', () => { + const script = `test("at start", function() { expect(true).to.be.true; });`; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + }); + + describe('should return false for invalid test() calls', () => { + it('should ignore commented out test calls with //', () => { + const script = ` + // test("commented test", function() { + // expect(true).to.be.true; + // }); + console.log("no real tests here"); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should ignore commented out test calls with /* */', () => { + const script = ` + /* test("block commented test", function() { + expect(true).to.be.true; + }); */ + console.log("no real tests here"); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should ignore test() in double-quoted strings', () => { + const script = ` + console.log("This contains test() but should not match"); + console.log("Remember to test() your API"); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should ignore test() in single-quoted strings', () => { + const script = ` + console.log('Single quote test() should not match'); + const message = 'Use test() for validation'; + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should ignore test() in template literals', () => { + const script = ` + console.log(\`Template literal test() should not match\`); + const message = \`Remember to test() your code\`; + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should ignore object method calls', () => { + const script = ` + const obj = { test: function() { return "not a real test"; } }; + obj.test("This is a method call"); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should ignore this.test() calls', () => { + const script = ` + this.test("Another method call"); + this.test(); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should ignore complex object chain calls', () => { + const script = ` + api.client.test("Should not match"); + user.test.endpoint("Chained method"); + window.test("Should not match"); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should ignore object methods in variables', () => { + const script = ` + const validator = { + test: function(value) { return value > 0; } + }; + validator.test(42); + + const tester = { test: () => "mock" }; + tester.test("method call"); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should return false for empty scripts', () => { + expect(hasExecutableTestInScript('')).toBe(false); + expect(hasExecutableTestInScript(null)).toBe(false); + expect(hasExecutableTestInScript(undefined)).toBe(false); + }); + + it('should return false for scripts with no test calls', () => { + const script = ` + bru.setVar("userId", "12345"); + console.log("Setting up request"); + const data = res.data; + bru.setVar("responseData", data); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should return false when test is part of other words', () => { + const script = ` + const testing = "value"; + const protest = "demo"; + const fastest = "speed"; + console.log("contest results"); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + }); + + describe('should handle mixed scenarios correctly', () => { + it('should return true when valid test exists among invalid ones', () => { + const script = ` + // test("commented out"); + console.log("test() in string"); + obj.test("method call"); + + test("real test", function() { + expect(true).to.be.true; + }); + + api.client.test("another method"); + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should return false when only invalid tests exist', () => { + const script = ` + // test("commented out test", function() { + // expect(true).to.be.true; + // }); + + console.log("test() inside string"); + console.log('test() in single quotes'); + console.log(\`test() in template\`); + + const obj = { test: () => "mock" }; + obj.test("method call"); + this.test("another method"); + api.client.test("chained method"); + + bru.setVar("test", "variable name"); + `; + expect(hasExecutableTestInScript(script)).toBe(false); + }); + + it('should handle complex nested quotes correctly', () => { + const script = ` + console.log("String with 'nested quotes' and test() call"); + console.log('String with "nested quotes" and test() call'); + + test("real test with \\"escaped quotes\\"", function() { + expect(true).to.be.true; + }); + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should handle multi-line comments correctly', () => { + const script = ` + /* + * This is a multi-line comment with + * test("commented test", function() { + * expect(true).to.be.true; + * }); + */ + + test("real test", function() { + expect(true).to.be.true; + }); + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + + it('should handle inline comments correctly', () => { + const script = ` + const data = res.data; // test("inline comment") + test("real test", function() { // this is a real test + expect(data).to.be.an("object"); + }); + `; + expect(hasExecutableTestInScript(script)).toBe(true); + }); + }); + + describe('edge cases', () => { + it('should handle test calls immediately after dots (edge case)', () => { + const script = ` + // This should not match because it's after a dot + console.test("should not match"); + + // But this should match because there's a space + console. test("should match due to space"); + `; + // Note: Our current implementation would consider the second one valid + // because there's a space between the dot and test + expect(hasExecutableTestInScript(script)).toBe(true); + }); + }); +}); \ No newline at end of file From d031687ee994d2991633e93d5121ede0c8f00375 Mon Sep 17 00:00:00 2001 From: Pooja Date: Thu, 7 Aug 2025 20:25:28 +0530 Subject: [PATCH 24/30] fix: url interpolation in code gen (#5187) --- .../CollectionItem/GenerateCodeItem/index.js | 9 ++++---- .../GenerateCodeItem/utils/interpolation.js | 21 ------------------- .../utils/snippet-generator.js | 14 ++----------- .../utils/snippet-generator.spec.js | 5 ++++- packages/bruno-app/src/utils/url/index.js | 13 ++---------- .../bruno-app/src/utils/url/index.spec.js | 12 ++--------- 6 files changed, 14 insertions(+), 60 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index aabaafcba..9f04daa9b 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -11,7 +11,7 @@ import { import { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index'; import { getLanguages } from 'utils/codegenerator/targets'; import { useSelector } from 'react-redux'; -import { getGlobalEnvironmentVariables } from 'utils/collections/index'; +import { getAllVariables, getGlobalEnvironmentVariables } from 'utils/collections/index'; import { resolveInheritedAuth } from './utils/auth-utils'; const GenerateCodeItem = ({ collectionUid, item, onClose }) => { @@ -37,12 +37,11 @@ const GenerateCodeItem = ({ collectionUid, item, onClose }) => { const requestUrl = get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url'); + const variables = getAllVariables(collection, item); + const interpolatedUrl = interpolateUrl({ url: requestUrl, - globalEnvironmentVariables, - envVars, - runtimeVariables: collection.runtimeVariables, - processEnvVars: collection.processEnvVariables + variables }); // interpolate the path params diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.js index 22a52f84f..e7081b268 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.js @@ -69,24 +69,3 @@ export const interpolateBody = (body, variables = {}) => { return interpolatedBody; }; - -export const createVariablesObject = ({ - globalEnvironmentVariables = {}, - collectionVars = {}, - allVariables = {}, - collection = {}, - runtimeVariables = {}, - processEnvVars = {} -}) => { - return { - ...globalEnvironmentVariables, - ...allVariables, - ...collectionVars, - ...runtimeVariables, - process: { - env: { - ...processEnvVars - } - } - }; -}; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js index 60f181ed1..41d9236ed 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js @@ -1,7 +1,7 @@ import { buildHarRequest } from 'utils/codegenerator/har'; import { getAuthHeaders } from 'utils/codegenerator/auth'; import { getAllVariables, getTreePathFromCollectionToItem } from 'utils/collections/index'; -import { interpolateHeaders, interpolateBody, createVariablesObject } from './interpolation'; +import { interpolateHeaders, interpolateBody } from './interpolation'; // Merge headers from collection, folders, and request const mergeHeaders = (collection, request, requestTreePath) => { @@ -46,17 +46,7 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false // Get HTTPSnippet dynamically so mocks can be applied in tests const { HTTPSnippet } = require('httpsnippet'); - const allVariables = getAllVariables(collection, item); - - // Create variables object for interpolation - const variables = createVariablesObject({ - globalEnvironmentVariables: collection.globalEnvironmentVariables || {}, - collectionVars: collection.collectionVars || {}, - allVariables, - collection, - runtimeVariables: collection.runtimeVariables || {}, - processEnvVars: collection.processEnvVariables || {} - }); + const variables = getAllVariables(collection, item); const request = item.request; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js index 941ea7a76..43581b2b4 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js @@ -46,7 +46,10 @@ jest.mock('utils/codegenerator/auth', () => ({ })); jest.mock('utils/collections/index', () => ({ - getAllVariables: jest.fn(() => ({ + getAllVariables: jest.fn((collection) => ({ + ...collection?.globalEnvironmentVariables, + ...collection?.runtimeVariables, + ...collection?.processEnvVariables, baseUrl: 'https://api.example.com', apiKey: 'secret-key-123', userId: '12345' diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index 7f9bdcc99..a8ac1b812 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -69,21 +69,12 @@ export const isValidUrl = (url) => { } }; -export const interpolateUrl = ({ url, globalEnvironmentVariables = {}, envVars, runtimeVariables, processEnvVars }) => { +export const interpolateUrl = ({ url, variables }) => { if (!url || !url.length || typeof url !== 'string') { return; } - return interpolate(url, { - ...globalEnvironmentVariables, - ...envVars, - ...runtimeVariables, - process: { - env: { - ...processEnvVars - } - } - }); + return interpolate(url, variables); }; export const interpolateUrlPathParams = (url, params) => { diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js index 8ecd0531d..bbcc919c8 100644 --- a/packages/bruno-app/src/utils/url/index.spec.js +++ b/packages/bruno-app/src/utils/url/index.spec.js @@ -77,11 +77,7 @@ describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => { const url = '{{host}}/api/:id/path?foo={{foo}}&bar={{bar}}&baz={{process.env.baz}}'; const expectedUrl = 'https://example.com/api/:id/path?foo=foo_value&bar=bar_value&baz=baz_value'; - const envVars = { host: 'https://example.com', foo: 'foo_value' }; - const runtimeVariables = { bar: 'bar_value' }; - const processEnvVars = { baz: 'baz_value' }; - - const result = interpolateUrl({ url, envVars, runtimeVariables, processEnvVars }); + const result = interpolateUrl({ url, variables: { host: 'https://example.com', foo: 'foo_value', bar: 'bar_value', 'process.env.baz': 'baz_value' } }); expect(result).toEqual(expectedUrl); }); @@ -101,11 +97,7 @@ describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => { const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }]; const expectedUrl = 'https://example.com/api/123/path?foo=foo_value&bar=bar_value&baz=baz_value'; - const envVars = { host: 'https://example.com', foo: 'foo_value' }; - const runtimeVariables = { bar: 'bar_value' }; - const processEnvVars = { baz: 'baz_value' }; - - const intermediateResult = interpolateUrl({ url, envVars, runtimeVariables, processEnvVars }); + const intermediateResult = interpolateUrl({ url, variables: { host: 'https://example.com', foo: 'foo_value', bar: 'bar_value', 'process.env.baz': 'baz_value' } }); const result = interpolateUrlPathParams(intermediateResult, params); expect(result).toEqual(expectedUrl); From 20ffae86e475789e580b2e4250377f40f9f31cb1 Mon Sep 17 00:00:00 2001 From: Andrew Borg Date: Fri, 8 Aug 2025 07:41:31 -0400 Subject: [PATCH 25/30] Add missing stringifyRequest import for bruno-cli (#5282) --- packages/bruno-cli/src/utils/collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index 185a682ba..79218eb50 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -3,7 +3,7 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); const { sanitizeName } = require('./filesystem'); -const { parseRequest, parseCollection, parseFolder, stringifyCollection, stringifyFolder, stringifyEnvironment } = require('@usebruno/filestore'); +const { parseRequest, parseCollection, parseFolder, stringifyCollection, stringifyFolder, stringifyEnvironment, stringifyRequest } = require('@usebruno/filestore'); const constants = require('../constants'); const chalk = require('chalk'); From f1f1c1fe5b2486040321f8129e20ef8f50427971 Mon Sep 17 00:00:00 2001 From: sreelakshmi-bruno Date: Fri, 8 Aug 2025 17:28:49 +0530 Subject: [PATCH 26/30] Handle decryption for secret env vars (#5285) --- package-lock.json | 196 ++++++++---------- .../src/app/collection-watcher.js | 10 +- .../bruno-electron/src/store/env-secrets.js | 4 +- .../src/store/global-environments.js | 6 +- packages/bruno-electron/src/store/oauth2.js | 8 +- .../bruno-electron/src/utils/encryption.js | 58 ++++-- .../tests/utils/encryption.spec.js | 68 +++++- 7 files changed, 217 insertions(+), 133 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ee290ea7..0d9a67cf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1603,7 +1603,7 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1621,7 +1621,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -1638,7 +1638,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1656,7 +1656,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@babel/helper-member-expression-to-functions": { @@ -1727,7 +1727,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1802,7 +1802,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -1845,7 +1845,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1862,7 +1862,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1878,7 +1878,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -1894,7 +1894,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1912,7 +1912,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -1947,7 +1947,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2046,7 +2046,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2062,7 +2062,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2244,7 +2244,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -2261,7 +2261,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2277,7 +2277,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2295,7 +2295,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -2313,7 +2313,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2329,7 +2329,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2361,7 +2361,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", @@ -2378,7 +2378,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -2399,7 +2399,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2409,7 +2409,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2426,7 +2426,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2442,7 +2442,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2459,7 +2459,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2475,7 +2475,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2492,7 +2492,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2508,7 +2508,7 @@ "version": "7.26.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2524,7 +2524,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2556,7 +2556,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2573,7 +2573,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -2591,7 +2591,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2607,7 +2607,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2623,7 +2623,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2639,7 +2639,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2655,7 +2655,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2688,7 +2688,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2707,7 +2707,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.25.9", @@ -2724,7 +2724,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -2741,7 +2741,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2772,7 +2772,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2788,7 +2788,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", @@ -2806,7 +2806,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -2823,7 +2823,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2855,7 +2855,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2887,7 +2887,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -2905,7 +2905,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -2990,7 +2990,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -3007,7 +3007,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3024,7 +3024,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3040,7 +3040,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3056,7 +3056,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", @@ -3073,7 +3073,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3089,7 +3089,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3105,7 +3105,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3140,7 +3140,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" @@ -3156,7 +3156,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3173,7 +3173,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3190,7 +3190,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", @@ -3207,7 +3207,7 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.26.0", @@ -3308,7 +3308,7 @@ "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -8524,6 +8524,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { @@ -8546,6 +8547,7 @@ "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/linkify-it": "*", @@ -8556,6 +8558,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, "license": "MIT" }, "node_modules/@types/ms": { @@ -9913,7 +9916,7 @@ "version": "0.4.12", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", @@ -9928,7 +9931,7 @@ "version": "0.10.6", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.2", @@ -9942,7 +9945,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3" @@ -12051,7 +12054,7 @@ "version": "3.39.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "browserslist": "^4.24.2" @@ -13788,6 +13791,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14385,7 +14389,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -16953,14 +16957,6 @@ "node": ">= 12" } }, - "node_modules/ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", - "engines": { - "node": ">=8" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -17086,7 +17082,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -17214,17 +17210,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-ip": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", - "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", - "dependencies": { - "ip-regex": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -19524,7 +19509,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/lodash.flow": { @@ -21544,7 +21529,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -23813,14 +23798,14 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -23839,7 +23824,7 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.4" @@ -23870,7 +23855,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -23888,14 +23873,14 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/regjsparser": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.0.2" @@ -23908,7 +23893,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -24082,7 +24067,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -26380,7 +26365,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -27219,7 +27204,7 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -27281,7 +27266,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -27291,7 +27276,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", @@ -27305,7 +27290,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -27315,7 +27300,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -31110,9 +31095,6 @@ "name": "@usebruno/common", "version": "0.1.0", "license": "MIT", - "dependencies": { - "is-ip": "^3.1.0" - }, "devDependencies": { "@babel/preset-env": "^7.26.9", "@babel/preset-typescript": "^7.27.0", diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js index e5f4503ac..acdb8873b 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -14,7 +14,7 @@ const { parseDotEnv } = require('@usebruno/filestore'); const { uuid } = require('../utils/common'); const { getRequestUid } = require('../cache/requestUids'); -const { decryptString } = require('../utils/encryption'); +const { decryptStringSafe } = require('../utils/encryption'); const { setDotEnvVars } = require('../store/process-env'); const { setBrunoConfig } = require('../store/bruno-config'); const EnvironmentSecretsStore = require('../store/env-secrets'); @@ -98,14 +98,15 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath) _.each(envSecrets, (secret) => { const variable = _.find(file.data.variables, (v) => v.name === secret.name); if (variable && secret.value) { - variable.value = decryptString(secret.value); + const decryptionResult = decryptStringSafe(secret.value); + variable.value = decryptionResult.value; } }); } win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file); } catch (err) { - console.error(err); + console.error('Error processing environment file: ', err); } }; @@ -132,7 +133,8 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat _.each(envSecrets, (secret) => { const variable = _.find(file.data.variables, (v) => v.name === secret.name); if (variable && secret.value) { - variable.value = decryptString(secret.value); + const decryptionResult = decryptStringSafe(secret.value); + variable.value = decryptionResult.value; } }); } diff --git a/packages/bruno-electron/src/store/env-secrets.js b/packages/bruno-electron/src/store/env-secrets.js index 894f7bc7a..1b962cdbd 100644 --- a/packages/bruno-electron/src/store/env-secrets.js +++ b/packages/bruno-electron/src/store/env-secrets.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const Store = require('electron-store'); -const { encryptString } = require('../utils/encryption'); +const { encryptStringSafe } = require('../utils/encryption'); /** * Sample secrets store file @@ -33,7 +33,7 @@ class EnvironmentSecretsStore { if (v.secret) { envVars.push({ name: v.name, - value: encryptString(v.value) + value: encryptStringSafe(v.value).value }); } }); diff --git a/packages/bruno-electron/src/store/global-environments.js b/packages/bruno-electron/src/store/global-environments.js index f9e9f175f..8a88674ef 100644 --- a/packages/bruno-electron/src/store/global-environments.js +++ b/packages/bruno-electron/src/store/global-environments.js @@ -1,6 +1,6 @@ const _ = require('lodash'); const Store = require('electron-store'); -const { encryptString, decryptString } = require('../utils/encryption'); +const { encryptStringSafe, decryptStringSafe } = require('../utils/encryption'); class GlobalEnvironmentsStore { constructor() { @@ -14,7 +14,7 @@ class GlobalEnvironmentsStore { return globalEnvironments?.map(env => { const variables = env.variables?.map(v => ({ ...v, - value: v?.secret ? encryptString(v.value) : v?.value + value: v?.secret ? encryptStringSafe(v.value).value : v?.value })) || []; return { @@ -28,7 +28,7 @@ class GlobalEnvironmentsStore { return globalEnvironments?.map(env => { const variables = env.variables?.map(v => ({ ...v, - value: v?.secret ? decryptString(v.value) : v?.value + value: v?.secret ? decryptStringSafe(v.value).value : v?.value })) || []; return { diff --git a/packages/bruno-electron/src/store/oauth2.js b/packages/bruno-electron/src/store/oauth2.js index 8c009db7b..d85573bc8 100644 --- a/packages/bruno-electron/src/store/oauth2.js +++ b/packages/bruno-electron/src/store/oauth2.js @@ -1,7 +1,7 @@ const _ = require('lodash'); const Store = require('electron-store'); const { uuid, safeStringifyJSON, safeParseJSON } = require('../utils/common'); -const { encryptString, decryptString } = require('../utils/encryption'); +const { encryptStringSafe, decryptStringSafe } = require('../utils/encryption'); /** * Sample secrets store file @@ -119,7 +119,8 @@ class Oauth2Store { let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url }); let credentials = oauth2DataForCollection?.credentials?.find(c => (c?.url == url) && (c?.credentialsId == credentialsId)); if (!credentials?.data) return null; - let decryptedCredentialsData = safeParseJSON(decryptString(credentials?.data)); + const decryptionResult = decryptStringSafe(credentials?.data); + const decryptedCredentialsData = safeParseJSON(decryptionResult.value); return decryptedCredentialsData; } catch (err) { console.log('error retrieving oauth2 credentials from cache', err); @@ -128,7 +129,8 @@ class Oauth2Store { updateCredentialsForCollection({ collectionUid, url, credentialsId, credentials = {} }) { try { - let encryptedCredentialsData = encryptString(safeStringifyJSON(credentials)); + const encryptionResult = encryptStringSafe(safeStringifyJSON(credentials)); + const encryptedCredentialsData = encryptionResult.value; let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url }); let filteredCredentials = oauth2DataForCollection?.credentials?.filter(c => (c?.url !== url) || (c?.credentialsId !== credentialsId)); if (!filteredCredentials) filteredCredentials = []; diff --git a/packages/bruno-electron/src/utils/encryption.js b/packages/bruno-electron/src/utils/encryption.js index 7e7b0b4b7..103f92155 100644 --- a/packages/bruno-electron/src/utils/encryption.js +++ b/packages/bruno-electron/src/utils/encryption.js @@ -54,11 +54,16 @@ function aes256Decrypt(data) { return decrypted; } catch (err) { // If decryption fails, fall back to old key derivation - const { key: oldKey, iv: oldIv } = deriveKeyAndIv(rawKey, 32, 16); - const decipher = crypto.createDecipheriv('aes-256-cbc', oldKey, oldIv); - let decrypted = decipher.update(data, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return decrypted; + try { + const { key: oldKey, iv: oldIv } = deriveKeyAndIv(rawKey, 32, 16); + const decipher = crypto.createDecipheriv('aes-256-cbc', oldKey, oldIv); + const decrypted = decipher.update(data, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } catch (fallbackErr) { + console.error('AES256 decryption failed with both methods:', err, fallbackErr); + throw new Error('AES256 decryption failed: ' + fallbackErr.message); + } } } @@ -73,16 +78,21 @@ function safeStorageEncrypt(str) { return encryptedString; } function safeStorageDecrypt(str) { - // Convert the hexadecimal string to a buffer - const encryptedStringBuffer = Buffer.from(str, 'hex'); + try { + // Convert the hexadecimal string to a buffer + const encryptedStringBuffer = Buffer.from(str, 'hex'); - // Decrypt the buffer - const decryptedStringBuffer = safeStorage.decryptString(encryptedStringBuffer); + // Decrypt the buffer + const decryptedStringBuffer = safeStorage.decryptString(encryptedStringBuffer); - // Convert the decrypted buffer to a string - const decryptedString = decryptedStringBuffer.toString(); + // Convert the decrypted buffer to a string + const decryptedString = decryptedStringBuffer.toString(); - return decryptedString; + return decryptedString; + } catch (err) { + console.error('SafeStorage decryption failed:', err); + throw new Error('SafeStorage decryption failed: ' + err.message); + } } function encryptString(str) { @@ -142,7 +152,29 @@ function decryptString(str) { } } +function decryptStringSafe(str) { + try { + const result = decryptString(str); + return { success: true, value: result }; + } catch (err) { + console.error('Decryption failed:', err.message); + return { success: false, error: err.message, value: '' }; + } +} + +function encryptStringSafe(str) { + try { + const result = encryptString(str); + return { success: true, value: result }; + } catch (err) { + console.error('Encryption failed:', err.message); + return { success: false, error: err.message, value: '' }; + } +} + module.exports = { encryptString, - decryptString + encryptStringSafe, + decryptString, + decryptStringSafe }; diff --git a/packages/bruno-electron/tests/utils/encryption.spec.js b/packages/bruno-electron/tests/utils/encryption.spec.js index ae13e8ee2..9ddc7f7c5 100644 --- a/packages/bruno-electron/tests/utils/encryption.spec.js +++ b/packages/bruno-electron/tests/utils/encryption.spec.js @@ -1,4 +1,4 @@ -const { encryptString, decryptString } = require('../../src/utils/encryption'); +const { encryptString, decryptString, encryptStringSafe, decryptStringSafe } = require('../../src/utils/encryption'); // We can only unit test aes 256 fallback as safeStorage is only available // in the main process @@ -45,3 +45,69 @@ describe('Encryption and Decryption Tests', () => { expect(() => decryptString(invalidAlgo)).toThrow('Decrypt failed: Invalid algo'); }); }); + +describe('Safe Encryption and Decryption Tests', () => { + it('should encrypt and decrypt successfully using encryptStringSafe and decryptStringSafe', () => { + const plaintext = 'bruno is awesome'; + const encryptionResult = encryptStringSafe(plaintext); + const decryptionResult = decryptStringSafe(encryptionResult.value); + + expect(encryptionResult.success).toBe(true); + expect(decryptionResult.success).toBe(true); + expect(decryptionResult.value).toBe(plaintext); + }); + + it('should handle empty strings in encryptStringSafe', () => { + const result = encryptStringSafe(''); + expect(result.success).toBe(true); + expect(result.value).toBe(''); + }); + + it('should handle empty strings in decryptStringSafe', () => { + const result = decryptStringSafe(''); + expect(result.success).toBe(true); + expect(result.value).toBe(''); + }); + + it('should handle invalid string format in decryptStringSafe', () => { + const result = decryptStringSafe('garbage'); + expect(result.success).toBe(false); + expect(result.error).toBe('Decrypt failed: unrecognized string format'); + expect(result.value).toBe(''); + }); + + it('should handle invalid algorithm in decryptStringSafe', () => { + const invalidAlgo = '$99:abcdefg'; + const result = decryptStringSafe(invalidAlgo); + expect(result.success).toBe(false); + expect(result.error).toBe('Decrypt failed: Invalid algo'); + expect(result.value).toBe(''); + }); + + it('should handle malformed encrypted string in decryptStringSafe', () => { + const malformedString = '$01:invalid-hex-string'; + const result = decryptStringSafe(malformedString); + expect(result.success).toBe(false); + expect(result.error).toContain('AES256 decryption failed'); + expect(result.value).toBe(''); + }); + + it('should handle special characters in encryptStringSafe and decryptStringSafe', () => { + const specialText = 'bruno@#$%^&*()_+-=[]{}|;:,.<>?'; + const encryptionResult = encryptStringSafe(specialText); + const decryptionResult = decryptStringSafe(encryptionResult.value); + + expect(encryptionResult.success).toBe(true); + expect(decryptionResult.success).toBe(true); + expect(decryptionResult.value).toBe(specialText); + }); + + it('decrypt-safe should not throw error for invalid inputs', () => { + expect(() => decryptStringSafe(null)).not.toThrow(); + expect(() => decryptStringSafe(undefined)).not.toThrow(); + expect(() => decryptStringSafe('garbage')).not.toThrow(); + expect(() => decryptStringSafe(123456789)).not.toThrow(); + expect(() => decryptStringSafe('aes256:')).not.toThrow(); + expect(() => decryptStringSafe('aes256:invalid_base64')).not.toThrow(); + }); +}); From 84cca6f92b0ac25a252704dc57640acd0fbc1805 Mon Sep 17 00:00:00 2001 From: Pooja Date: Fri, 8 Aug 2025 19:44:47 +0530 Subject: [PATCH 27/30] add: bulk edit for collection and folder header (#5279) --- .../CollectionSettings/Headers/index.js | 42 ++++++++++++++++--- .../FolderSettings/Headers/index.js | 41 +++++++++++++++--- .../ReduxStore/slices/collections/index.js | 39 +++++++++++++++++ 3 files changed, 112 insertions(+), 10 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Headers/index.js b/packages/bruno-app/src/components/CollectionSettings/Headers/index.js index 9ae6e1e07..d0968c425 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Headers/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Headers/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import get from 'lodash/get'; import cloneDeep from 'lodash/cloneDeep'; import { IconTrash } from '@tabler/icons'; @@ -7,19 +7,30 @@ import { useTheme } from 'providers/Theme'; import { addCollectionHeader, updateCollectionHeader, - deleteCollectionHeader + deleteCollectionHeader, + setCollectionHeaders } from 'providers/ReduxStore/slices/collections'; import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; import SingleLineEditor from 'components/SingleLineEditor'; import StyledWrapper from './StyledWrapper'; import { headers as StandardHTTPHeaders } from 'know-your-http-well'; import { MimeTypes } from 'utils/codemirror/autocompleteConstants'; +import BulkEditor from 'components/BulkEditor/index'; const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header); const Headers = ({ collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); const headers = get(collection, 'root.request.headers', []); + const [isBulkEditMode, setIsBulkEditMode] = useState(false); + + const toggleBulkEditMode = () => { + setIsBulkEditMode(!isBulkEditMode); + }; + + const handleBulkHeadersChange = (newHeaders) => { + dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: newHeaders })); + }; const addHeader = () => { dispatch( @@ -63,6 +74,22 @@ const Headers = ({ collection }) => { ); }; + if (isBulkEditMode) { + return ( + +
+ Add request headers that will be sent with every request in this collection. +
+ +
+ ); + } + return (
@@ -141,9 +168,14 @@ const Headers = ({ collection }) => { : null} - +
+ + +
+
+ + +