Merge remote-tracking branch 'origin/main' into oauth2_additional_params

This commit is contained in:
lohit-bruno
2025-08-19 17:39:02 +05:30
22 changed files with 612 additions and 83 deletions

33
package-lock.json generated
View File

@@ -13937,6 +13937,21 @@
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
@@ -15411,13 +15426,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -16010,14 +16027,16 @@
}
},
"node_modules/graphql-request/node_modules/form-data": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz",
"integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
"integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.35"
},
"engines": {
"node": ">= 6"

View File

@@ -111,5 +111,10 @@
"tailwindcss": "^3.4.1",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
},
"overrides": {
"httpsnippet": {
"form-data": "4.0.4"
}
}
}

View File

@@ -1,16 +1,13 @@
import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons';
import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions';
import { loadLargeRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const RequestNotLoaded = ({ collection, item }) => {
const dispatch = useDispatch();
const handleLoadRequestViaWorker = () => {
!item?.loading && dispatch(loadRequestViaWorker({ collectionUid: collection?.uid, pathname: item?.pathname }));
}
const handleLoadRequest = () => {
!item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
const handleLoadLargeRequest = () => {
!item?.loading && dispatch(loadLargeRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
}
return <StyledWrapper>
@@ -44,23 +41,14 @@ const RequestNotLoaded = ({ collection, item }) => {
<IconAlertTriangle size={16} className="text-yellow-500" />
<span>The request wasn't loaded due to its large size. Please try again with the following options:</span>
</div>
<div className='flex flex-row mt-6 gap-2 items-center w-full'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequestViaWorker}
>
Load in background
</button>
<p>(Runs in background)</p>
</div>
<div className='flex flex-row mt-6 items-center gap-2 w-full'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequest}
onClick={handleLoadLargeRequest}
>
Force load
Load Request
</button>
<p>(May cause the app to freeze temporarily while it runs)</p>
<p>(Uses a regex based parsing approach)</p>
</div>
</div>
)}

View File

@@ -75,12 +75,15 @@ export default function RunnerResults({ collection }) {
useEffect(() => {
const savedConfiguration = get(collection, 'runnerConfiguration', null);
if (savedConfiguration && configureMode) {
if (savedConfiguration.selectedRequestItems) {
if (savedConfiguration) {
if (savedConfiguration.selectedRequestItems && configureMode) {
setSelectedRequestItems(savedConfiguration.selectedRequestItems);
}
if (savedConfiguration.delay !== undefined && delay === null) {
setDelay(savedConfiguration.delay);
}
}
}, [collection.runnerConfiguration, configureMode]);
}, [collection.runnerConfiguration, configureMode, delay]);
const collectionCopy = cloneDeep(collection);
const runnerInfo = get(collection, 'runnerResult.info', {});
@@ -136,9 +139,10 @@ export default function RunnerResults({ collection }) {
const runCollection = () => {
if (configureMode && selectedRequestItems.length > 0) {
dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, selectedRequestItems));
dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, selectedRequestItems, delay));
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags, selectedRequestItems));
} else {
dispatch(updateRunnerConfiguration(collection.uid, [], [], delay));
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));
}
};
@@ -148,12 +152,13 @@ export default function RunnerResults({ collection }) {
// Get the saved configuration to determine what to run
const savedConfiguration = get(collection, 'runnerConfiguration', null);
const savedSelectedItems = savedConfiguration?.selectedRequestItems || [];
const savedDelay = savedConfiguration?.delay !== undefined ? savedConfiguration.delay : delay;
dispatch(
runCollectionFolder(
collection.uid,
runnerInfo.folderUid,
true,
Number(delay),
Number(savedDelay),
tagsEnabled && tags,
savedSelectedItems
)
@@ -168,6 +173,7 @@ export default function RunnerResults({ collection }) {
);
setSelectedRequestItems([]);
setConfigureMode(false);
setDelay(null);
};
const cancelExecution = () => {

View File

@@ -5,10 +5,10 @@ const KeyMapping = {
newRequest: { mac: 'command+b', windows: 'ctrl+b', name: 'New Request' },
closeTab: { mac: 'command+w', windows: 'ctrl+w', name: 'Close Tab' },
openPreferences: { mac: 'command+,', windows: 'ctrl+,', name: 'Open Preferences' },
minimizeWindow: {
mac: 'command+Shift+Q',
windows: 'control+Shift+Q',
name: 'Minimize Window'
closeBruno: {
mac: 'command+Q',
windows: 'ctrl+shift+q',
name: 'Close Bruno'
},
switchToPreviousTab: {
mac: 'command+pageup',

View File

@@ -1361,6 +1361,7 @@ export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
});
};
// todo: could be removed
export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
@@ -1368,6 +1369,7 @@ export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch,
});
};
// todo: could be removed
export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
@@ -1375,6 +1377,13 @@ export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState)
});
};
export const loadLargeRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-large-request', { collectionUid, pathname }).then(resolve).catch(reject);
});
};
export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig }) => (dispatch, getState) => {
dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounting' }));
return new Promise(async (resolve, reject) => {
@@ -1395,10 +1404,11 @@ export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig
});
};
export const updateRunnerConfiguration = (collectionUid, selectedRequestItems, requestItemsOrder) => (dispatch) => {
export const updateRunnerConfiguration = (collectionUid, selectedRequestItems, requestItemsOrder, delay) => (dispatch) => {
dispatch(_updateRunnerConfiguration({
collectionUid,
selectedRequestItems,
requestItemsOrder
requestItemsOrder,
delay
}));
};

View File

@@ -2291,12 +2291,13 @@ export const collectionsSlice = createSlice({
}
},
updateRunnerConfiguration: (state, action) => {
const { collectionUid, selectedRequestItems, requestItemsOrder } = action.payload;
const { collectionUid, selectedRequestItems, requestItemsOrder, delay } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.runnerConfiguration = {
selectedRequestItems: selectedRequestItems || [],
requestItemsOrder: requestItemsOrder || []
requestItemsOrder: requestItemsOrder || [],
delay: delay
};
}
},

View File

@@ -57,7 +57,7 @@ const transformOpenapiRequestItem = (request) => {
url: ensureUrl(request.global.server + path),
method: request.method.toUpperCase(),
auth: {
mode: 'none',
mode: 'inherit',
basic: null,
bearer: null,
digest: null
@@ -419,6 +419,19 @@ export const parseOpenApiCollection = (data) => {
uid: uuid(),
name: group.name,
type: 'folder',
root: {
request: {
auth: {
mode: 'inherit',
basic: null,
bearer: null,
digest: null
}
},
meta: {
name: group.name
}
},
items: group.requests.map(transformOpenapiRequestItem)
};
});

View File

@@ -233,7 +233,7 @@ const circularRefsOutput = {
"url": "{{baseUrl}}/",
"method": "POST",
"auth": {
"mode": "none",
"mode": "inherit",
},
"headers": [],
"params": [],

View File

@@ -8,6 +8,13 @@ describe('openapi-collection', () => {
expect(brunoCollection).toMatchObject(expectedOutput);
});
it('should set auth mode to inherit when no security is defined in the collection', () => {
const brunoCollection = openApiToBruno(openApiCollectionString);
// The openApiCollectionString has no security defined, so auth mode should be 'inherit'
expect(brunoCollection.items[0].items[0].request.auth.mode).toBe('inherit');
});
it('trims whitespace from info.title and uses the trimmed value as the collection name', () => {
const openApiWithTitle = `
openapi: '3.0.0'
@@ -109,6 +116,124 @@ servers:
expect(result.name).toBe('Untitled Collection');
});
describe('authentication inheritance', () => {
it('should set auth mode to inherit when no security is defined', () => {
const openApiWithoutSecurity = `
openapi: '3.0.0'
info:
version: '1.0.0'
title: 'API without security'
paths:
/test:
get:
summary: 'Test endpoint'
operationId: 'testEndpoint'
responses:
'200':
description: 'OK'
servers:
- url: 'https://example.com'
`;
const result = openApiToBruno(openApiWithoutSecurity);
expect(result.items[0].request.auth.mode).toBe('inherit');
});
it('should set auth mode to inherit when no global security schemes exist', () => {
const openApiWithEmptySecurity = `
openapi: '3.0.0'
info:
version: '1.0.0'
title: 'API with empty security'
security: []
paths:
/test:
get:
summary: 'Test endpoint'
operationId: 'testEndpoint'
responses:
'200':
description: 'OK'
servers:
- url: 'https://example.com'
`;
const result = openApiToBruno(openApiWithEmptySecurity);
expect(result.items[0].request.auth.mode).toBe('inherit');
});
it('should set auth mode to inherit when components.securitySchemes is empty', () => {
const openApiWithEmptyComponents = `
openapi: '3.0.0'
info:
version: '1.0.0'
title: 'API with empty components'
components:
securitySchemes: {}
paths:
/test:
get:
summary: 'Test endpoint'
operationId: 'testEndpoint'
responses:
'200':
description: 'OK'
servers:
- url: 'https://example.com'
`;
const result = openApiToBruno(openApiWithEmptyComponents);
expect(result.items[0].request.auth.mode).toBe('inherit');
});
it('should set auth mode to inherit when operation has empty security array', () => {
const openApiWithEmptyOperationSecurity = `
openapi: '3.0.0'
info:
version: '1.0.0'
title: 'API with empty operation security'
components:
securitySchemes:
basicAuth:
type: http
scheme: basic
paths:
/test:
get:
summary: 'Test endpoint'
operationId: 'testEndpoint'
security: []
responses:
'200':
description: 'OK'
servers:
- url: 'https://example.com'
`;
const result = openApiToBruno(openApiWithEmptyOperationSecurity);
expect(result.items[0].request.auth.mode).toBe('inherit');
});
it('should set auth mode to inherit for folder root when no security is defined', () => {
const openApiWithTags = `
openapi: '3.0.0'
info:
version: '1.0.0'
title: 'API with tags'
paths:
/test:
get:
tags:
- TestGroup
summary: 'Test endpoint'
operationId: 'testEndpoint'
responses:
'200':
description: 'OK'
servers:
- url: 'https://example.com'
`;
const result = openApiToBruno(openApiWithTags);
expect(result.items[0].type).toBe('folder');
expect(result.items[0].root.request.auth.mode).toBe('inherit');
});
});
});
const openApiCollectionString = `
@@ -174,7 +299,7 @@ const expectedOutput = {
"basic": null,
"bearer": null,
"digest": null,
"mode": "none",
"mode": "inherit",
},
"body": {
"formUrlEncoded": [],

View File

@@ -20,6 +20,7 @@ const { setBrunoConfig } = require('../store/bruno-config');
const EnvironmentSecretsStore = require('../store/env-secrets');
const UiStateSnapshot = require('../store/ui-state-snapshot');
const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
const { parseLargeRequestWithRedaction } = require('../utils/parse');
const MAX_FILE_SIZE = 2.5 * 1024 * 1024;
@@ -344,11 +345,17 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
let seq;
const folderBruFilePath = path.join(pathname, `folder.bru`);
if (fs.existsSync(folderBruFilePath)) {
let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8');
let folderBruData = await parseFolder(folderBruFileContent);
name = folderBruData?.meta?.name || name;
seq = folderBruData?.meta?.seq;
try {
if (fs.existsSync(folderBruFilePath)) {
let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8');
let folderBruData = await parseFolder(folderBruFileContent);
name = folderBruData?.meta?.name || name;
seq = folderBruData?.meta?.seq;
}
}
catch(error) {
console.error('Error occured while parsing folder.bru file!');
console.error(error);
}
const directory = {
@@ -462,10 +469,20 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
};
const bru = fs.readFileSync(pathname, 'utf8');
file.data = await parseRequest(bru);
const fileStats = fs.statSync(pathname);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
if (fileStats.size >= MAX_FILE_SIZE) {
const parsedData = await parseLargeRequestWithRedaction(bru);
file.data = parsedData;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
} else {
file.data = await parseRequest(bru);
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
}
} catch (err) {
console.error(err);
}

View File

@@ -11,7 +11,7 @@ if (isDev) {
}
const { format } = require('url');
const { BrowserWindow, app, session, Menu, ipcMain } = require('electron');
const { BrowserWindow, app, session, Menu, globalShortcut, ipcMain } = require('electron');
const { setContentSecurityPolicy } = require('electron-util');
if (isDev && process.env.ELECTRON_USER_DATA_PATH) {
@@ -165,6 +165,19 @@ app.on('ready', async () => {
}
return { action: 'deny' };
});
// Quick fix for Electron issue #29996: https://github.com/electron/electron/issues/29996
globalShortcut.register('Ctrl+=', () => {
mainWindow.webContents.setZoomLevel(mainWindow.webContents.getZoomLevel() + 1);
});
globalShortcut.register('CommandOrControl+M', () => {
mainWindow.minimize();
});
globalShortcut.register('CommandOrControl+H', () => {
mainWindow.minimize();
});
mainWindow.webContents.on('did-finish-load', () => {
let ogSend = mainWindow.webContents.send;

View File

@@ -19,6 +19,7 @@ const {
} = require('@usebruno/filestore');
const brunoConverters = require('@usebruno/converters');
const { postmanToBruno } = brunoConverters;
const { parseLargeRequestWithRedaction } = require('../utils/parse');
const {
writeFile,
@@ -1057,6 +1058,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
// todo: could be removed
ipcMain.handle('renderer:load-request-via-worker', async (event, { collectionUid, pathname }) => {
let fileStats;
try {
@@ -1094,7 +1096,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
};
let bruContent = fs.readFileSync(pathname, 'utf8');
const metaJson = parseRequest(parseBruFileMeta(bruContent));
const metaJson = parseBruFileMeta(bruContent);
file.data = metaJson;
file.partial = true;
file.loading = false;
@@ -1132,6 +1134,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
// todo: could be removed
ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => {
let fileStats;
try {
@@ -1145,7 +1148,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
};
let bruContent = fs.readFileSync(pathname, 'utf8');
const metaJson = parseRequest(parseBruFileMeta(bruContent));
const metaJson = parseBruFileMeta(bruContent);
file.data = metaJson;
file.loading = true;
file.partial = true;
@@ -1169,7 +1172,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
};
let bruContent = fs.readFileSync(pathname, 'utf8');
const metaJson = parseRequest(parseBruFileMeta(bruContent));
const metaJson = parseBruFileMeta(bruContent);
file.data = metaJson;
file.partial = true;
file.loading = false;
@@ -1181,6 +1184,56 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
ipcMain.handle('renderer:load-large-request', async (event, { collectionUid, pathname }) => {
let fileStats;
if (!hasBruExtension(pathname)) {
return;
}
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname)
}
};
try {
fileStats = fs.statSync(pathname);
const bruContent = fs.readFileSync(pathname, 'utf8');
const metaJson = parseBruFileMeta(bruContent);
file.data = metaJson;
file.partial = false;
file.loading = true;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
try {
const parsedData = await parseLargeRequestWithRedaction(bruContent);
file.data = parsedData;
file.loading = false;
file.partial = false;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
} catch (parseError) {
file.data = metaJson;
file.partial = true;
file.loading = false;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
throw parseError;
}
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:mount-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => {
const {
size,

View File

@@ -252,23 +252,24 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
'Accept': 'application/json',
};
if (credentialsPlacement === "basic_auth_header") {
axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString('base64')}`;
}
const data = {
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: callbackUrl,
client_id: clientId,
};
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
if (credentialsPlacement !== "basic_auth_header") {
data.client_id = clientId;
}
if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== "basic_auth_header") {
data.client_secret = clientSecret;
}
if (pkce) {
data['code_verifier'] = codeVerifier;
}
if (scope && scope.trim() !== '') {
data.scope = scope;
}
axiosRequestConfig.data = qs.stringify(data);
axiosRequestConfig.url = url;
axiosRequestConfig.responseType = 'arraybuffer';
// Apply additional parameters to token request
@@ -392,15 +393,6 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
};
}
if (!clientSecret) {
return {
error: 'Client Secret is required for OAuth2 client credentials flow',
credentials: null,
url,
credentialsId
};
}
if (!forceFetch) {
const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
@@ -459,14 +451,16 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
'content-type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
};
if (credentialsPlacement === "basic_auth_header") {
axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
if (credentialsPlacement === "basic_auth_header" && clientSecret && clientSecret.trim() !== '') {
axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString('base64')}`;
}
const data = {
grant_type: 'client_credentials',
client_id: clientId,
};
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
if (credentialsPlacement !== "basic_auth_header") {
data.client_id = clientId;
}
if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== "basic_auth_header") {
data.client_secret = clientSecret;
}
if (scope && scope.trim() !== '') {
@@ -604,16 +598,18 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
'content-type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
};
if (credentialsPlacement === "basic_auth_header") {
axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`;
if (credentialsPlacement === "basic_auth_header" && clientSecret && clientSecret.trim() !== '') {
axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString('base64')}`;
}
const data = {
grant_type: 'password',
username,
password,
client_id: clientId,
};
if (clientSecret && credentialsPlacement !== "basic_auth_header") {
if (credentialsPlacement !== "basic_auth_header") {
data.client_id = clientId;
}
if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== "basic_auth_header") {
data.client_secret = clientSecret;
}
if (scope && scope.trim() !== '') {
@@ -638,7 +634,7 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyConfig }) => {
const oAuth = get(requestCopy, 'oauth2', {});
const { clientId, clientSecret, credentialsId, additionalParameters } = oAuth;
const { clientId, clientSecret, credentialsId, credentialsPlacement, additionalParameters } = oAuth;
const url = oAuth.refreshTokenUrl ? oAuth.refreshTokenUrl : oAuth.accessTokenUrl;
const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });
@@ -649,10 +645,12 @@ const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyCon
} else {
const data = {
grant_type: 'refresh_token',
client_id: clientId,
refresh_token: credentials.refresh_token,
};
if (clientSecret) {
if (credentialsPlacement !== "basic_auth_header") {
data.client_id = clientId;
}
if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== "basic_auth_header") {
data.client_secret = clientSecret;
}
let axiosRequestConfig = {};
@@ -661,6 +659,9 @@ const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyCon
'content-type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
};
if (credentialsPlacement === "basic_auth_header") {
axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`).toString('base64')}`;
}
axiosRequestConfig.url = url;
axiosRequestConfig.responseType = 'arraybuffer';
if (additionalParameters?.refresh?.length) {

View File

@@ -0,0 +1,42 @@
const { parseRequestAndRedactBody, parseRequestViaWorker } = require('@usebruno/filestore');
/**
* Parses a large BRU request string by redacting body blocks, parsing the remainder,
* and then reinserting extracted body content into the parsed structure.
* @param {string} bruContent
* @returns {Promise<any>} parsed request JSON
*/
async function parseLargeRequestWithRedaction(bruContent) {
const { bruFileStringWithRedactedBody, extractedBodyContent } = parseRequestAndRedactBody(bruContent);
const parsedData = await parseRequestViaWorker(bruFileStringWithRedactedBody);
if (!parsedData.request) {
parsedData.request = {};
}
if (!parsedData.request.body) {
parsedData.request.body = {};
}
if (extractedBodyContent.json) {
parsedData.request.body.json = extractedBodyContent.json;
}
if (extractedBodyContent.text) {
parsedData.request.body.text = extractedBodyContent.text;
}
if (extractedBodyContent.xml) {
parsedData.request.body.xml = extractedBodyContent.xml;
}
if (extractedBodyContent.sparql) {
parsedData.request.body.sparql = extractedBodyContent.sparql;
}
if (extractedBodyContent.graphql) {
if (!parsedData.request.body.graphql) {
parsedData.request.body.graphql = {};
}
parsedData.request.body.graphql.query = extractedBodyContent.graphql;
}
return parsedData;
}
module.exports = { parseLargeRequestWithRedaction };

View File

@@ -58,8 +58,8 @@ export const bruRequestToJson = (data: string | any, parsed: boolean = false): a
}
return transformedJson;
} catch (e) {
return Promise.reject(e);
} catch (error) {
throw error;
}
};

View File

@@ -0,0 +1,66 @@
meta {
name: echo request
type: http
seq: 1
}
post {
url: https://echo.usebruno.com
body: json
auth: none
}
body:json {
{
"hello": "world"
}
}
body:text {
This is a text body
}
body:xml {
<xml>
<name>John</name>
<age>30</age>
</xml>
}
body:sparql {
SELECT * WHERE {
?subject ?predicate ?object .
}
LIMIT 10
}
body:graphql {
{
launchesPast {
launch_site {
site_name
}
launch_success
}
}
}
body:form-urlencoded {
apikey: secret
numbers: +91998877665
~message: hello
}
body:multipart-form {
apikey: secret
numbers: +91998877665
~message: hello
}
body:file {
file: @file(path/to/file.json) @contentType(application/json)
file: @file(path/to/file.json) @contentType(application/json)
~file: @file(path/to/file2.json) @contentType(application/json)
}
body:graphql:vars {
{
"limit": 5
}
}

View File

@@ -0,0 +1,35 @@
meta {
name: echo request
type: http
seq: 1
}
post {
url: https://echo.usebruno.com
body: json
auth: none
}
body:form-urlencoded {
apikey: secret
numbers: +91998877665
~message: hello
}
body:multipart-form {
apikey: secret
numbers: +91998877665
~message: hello
}
body:file {
file: @file(path/to/file.json) @contentType(application/json)
file: @file(path/to/file.json) @contentType(application/json)
~file: @file(path/to/file2.json) @contentType(application/json)
}
body:graphql:vars {
{
"limit": 5
}
}

View File

@@ -0,0 +1,44 @@
const fs = require('node:fs');
const path = require('node:path');
const { bruRequestParseAndRedactBodyData } = require("../utils/request-parse-and-redact-body-data");
describe("parse and redact body data", () => {
it("should redact body blocks from the bru file string", () => {
const fixturesPath = `/fixtures/request-parse-and-redact-body-data`;
const inputBruString = fs.readFileSync(path.join(__dirname, fixturesPath, './input.bru'), 'utf8');
const expectedOutputBruString = fs.readFileSync(path.join(__dirname, fixturesPath, './output.bru'), 'utf8');
const res = bruRequestParseAndRedactBodyData(inputBruString);
expect(res.bruFileStringWithRedactedBody).toBe(expectedOutputBruString);
expect(res.extractedBodyContent).toEqual({
graphql: `
{
launchesPast {
launch_site {
site_name
}
launch_success
}
}
`.trim(),
json: `
{
"hello": "world"
}
`.trim(),
sparql: `
SELECT * WHERE {
?subject ?predicate ?object .
}
LIMIT 10
`.trim(),
text: `This is a text body`,
xml: `
<xml>
<name>John</name>
<age>30</age>
</xml>
`.trim()
})
});
});

View File

@@ -0,0 +1,77 @@
/**
* Parses a .bru file and extracts body content while redacting it from the main content
* @param {string} bruFileContent - The raw content of the .bru file
* @returns {Object} Object containing redacted file content and extracted body data
*/
export const bruRequestParseAndRedactBodyData = (bruFileContent: string) => {
try {
// Define the patterns that indicate the start of different body types
const bodyTypePatterns = [
"body:json {",
"body:text {",
"body:xml {",
"body:sparql {",
"body:graphql {"
];
// Normalize line endings to LF
bruFileContent = (bruFileContent || '').replace(/\r\n/g, '\n');
const EOL = `\n`;
/**
* Removes the leading 2-space indentation from each line of a string
* @param {string} indentedString - The string with leading spaces to remove
* @returns {string} The string with indentation removed
*/
const removeLeadingIndentation = (indentedString: string) => {
if (!indentedString || !indentedString.length) {
return indentedString || '';
}
return indentedString
.split(EOL)
.map((line) => line.replace(/^ /, ''))
.join(EOL);
};
// Split the file content into blocks
let fileContentBlocks = bruFileContent.split(`${EOL}}${EOL}`);
fileContentBlocks = fileContentBlocks.filter(Boolean).map(_ => _.trim());
// Extract body blocks and their content
const extractedBodyBlocks = fileContentBlocks
.filter(block => bodyTypePatterns.some(pattern => block.startsWith(pattern)))
.reduce((bodyContentMap: Record<string, string>, bodyBlock) => {
// Extract the body type (json, text, xml, etc.) from the first line
const firstLine = bodyBlock.split(EOL)[0];
const bodyType = firstLine.split(`body:`)[1].split(/\s/)[0];
// Extract the body content (everything between the opening and closing braces)
const bodyContentLines = bodyBlock.split(EOL).slice(1);
const rawBodyContent = bodyContentLines.join(EOL);
// Remove indentation from the body content
const cleanBodyContent = removeLeadingIndentation(rawBodyContent);
bodyContentMap[bodyType] = cleanBodyContent;
return bodyContentMap;
}, {});
// Filter out body blocks to get the remaining file content
const fileContentWithoutBodyBlocks = fileContentBlocks.filter(block =>
!bodyTypePatterns.some(pattern => block.startsWith(pattern))
);
return {
bruFileStringWithRedactedBody: fileContentWithoutBodyBlocks.join(`${EOL}}${EOL}${EOL}`).concat(`${EOL}}${EOL}`),
extractedBodyContent: extractedBodyBlocks
};
} catch (error) {
console.error('Error parsing and redacting body data:', error);
return {
bruFileStringWithRedactedBody: bruFileContent,
extractedBodyContent: {}
};
}
};

View File

@@ -15,6 +15,7 @@ import {
ParsedCollection,
ParsedEnvironment
} from './types';
import { bruRequestParseAndRedactBodyData } from './formats/bru/utils/request-parse-and-redact-body-data';
export const parseRequest = (content: string, options: ParseOptions = { format: 'bru' }): any => {
if (options.format === 'bru') {
@@ -23,6 +24,13 @@ export const parseRequest = (content: string, options: ParseOptions = { format:
throw new Error(`Unsupported format: ${options.format}`);
};
export const parseRequestAndRedactBody = (content: string, options: ParseOptions = { format: 'bru' }): any => {
if (options.format === 'bru') {
return bruRequestParseAndRedactBodyData(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);

View File

@@ -10,10 +10,16 @@ get {
auth: inherit
}
script:pre-request {
const jar = bru.cookies.jar()
jar.setCookie("https://testbench-sanity.usebruno.com", "name", "value")
}
tests {
const jar = bru.cookies.jar()
jar.getCookie("https://testbench-sanity.usebruno.com", "__cf_bm", function(error, data) {
jar.getCookie("https://testbench-sanity.usebruno.com", "name", function(error, data) {
if(error) {
console.error("Cookie retrieval error:", error)
throw new Error(`Failed to get cookie: ${error.message || error}`)
@@ -22,7 +28,7 @@ tests {
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.key).to.equal("name");
expect(data.value).to.be.a('string');
expect(data.value).to.not.be.empty;
expect(data.domain).to.include('usebruno.com');