const _ = require('lodash'); const fs = require('fs'); 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 { writeFile, hasBruExtension, isDirectory, browseDirectory, browseFiles, createDirectory, searchForBruFiles, sanitizeName, isWSLPath, safeToRename, isWindowsOS, validateName, hasSubDirectories, getCollectionStats, sizeInMB, safeWriteFileSync } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids'); const { deleteCookiesForDomain, getDomainsWithCookies, addCookieForDomain, modifyCookieForDomain, parseCookieString, createCookieString, deleteCookie } = require('../utils/cookies'); const EnvironmentSecretsStore = require('../store/env-secrets'); const CollectionSecurityStore = require('../store/collection-security'); const UiStateSnapshotStore = require('../store/ui-state-snapshot'); const Oauth2Store = require('../store/oauth2'); const interpolateVars = require('./network/interpolate-vars'); const { getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../utils/collection'); const { getProcessEnvVars } = require('../store/process-env'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, refreshOauth2Token } = require('../utils/oauth2'); const { configureRequestWithCertsAndProxy } = require('./network'); const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection'); const environmentSecretsStore = new EnvironmentSecretsStore(); const collectionSecurityStore = new CollectionSecurityStore(); const uiStateSnapshotStore = new UiStateSnapshotStore(); // size and file count limits to determine whether the bru files in the collection should be loaded asynchronously or not. const MAX_COLLECTION_SIZE_IN_MB = 20; const MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 5; const MAX_COLLECTION_FILES_COUNT = 2000; const envHasSecrets = (environment = {}) => { const secrets = _.filter(environment.variables, (v) => v.secret); return secrets && secrets.length > 0; }; const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { // browse directory ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => { try { return await browseDirectory(mainWindow); } catch (error) { return Promise.reject(error); } }); // browse directory for file ipcMain.handle('renderer:browse-files', async (_, filters, properties) => { try { return await browseFiles(mainWindow, filters, properties); } catch (error) { throw error; } }); // create collection ipcMain.handle( 'renderer:create-collection', async (event, collectionName, collectionFolderName, collectionLocation) => { try { collectionFolderName = sanitizeName(collectionFolderName); const dirPath = path.join(collectionLocation, collectionFolderName); if (fs.existsSync(dirPath)) { const files = fs.readdirSync(dirPath); if (files.length > 0) { throw new Error(`collection: ${dirPath} already exists and is not empty`); } } if (!validateName(path.basename(dirPath))) { throw new Error(`collection: invalid pathname - ${dirPath}`); } if (!fs.existsSync(dirPath)) { await createDirectory(dirPath); } const uid = generateUidBasedOnHash(dirPath); const brunoConfig = { version: '1', name: collectionName, type: 'collection', ignore: ['node_modules', '.git'] }; const content = await stringifyJson(brunoConfig); await writeFile(path.join(dirPath, 'bruno.json'), content); const { size, filesCount } = await getCollectionStats(dirPath); brunoConfig.size = size; brunoConfig.filesCount = filesCount; mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig); ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid, brunoConfig); } catch (error) { return Promise.reject(error); } } ); // clone collection ipcMain.handle( 'renderer:clone-collection', async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => { collectionFolderName = sanitizeName(collectionFolderName); const dirPath = path.join(collectionLocation, collectionFolderName); if (fs.existsSync(dirPath)) { throw new Error(`collection: ${dirPath} already exists`); } if (!validateName(path.basename(dirPath))) { throw new Error(`collection: invalid pathname - ${dirPath}`); } // create dir await createDirectory(dirPath); const uid = generateUidBasedOnHash(dirPath); // open the bruno.json of previousPath const brunoJsonFilePath = path.join(previousPath, 'bruno.json'); const content = fs.readFileSync(brunoJsonFilePath, 'utf8'); // Change new name of collection let brunoConfig = JSON.parse(content); brunoConfig.name = collectionName; const cont = await stringifyJson(brunoConfig); // write the bruno.json to new dir await writeFile(path.join(dirPath, 'bruno.json'), cont); // Now copy all the files with extension name .bru along with the dir const files = searchForBruFiles(previousPath); for (const sourceFilePath of files) { const relativePath = path.relative(previousPath, sourceFilePath); const newFilePath = path.join(dirPath, relativePath); // handle dir of files fs.mkdirSync(path.dirname(newFilePath), { recursive: true }); // copy each files fs.copyFileSync(sourceFilePath, newFilePath); } const { size, filesCount } = await getCollectionStats(dirPath); brunoConfig.size = size; brunoConfig.filesCount = filesCount; mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig); ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid); } ); // rename collection ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => { try { const brunoJsonFilePath = path.join(collectionPathname, 'bruno.json'); const content = fs.readFileSync(brunoJsonFilePath, 'utf8'); const json = JSON.parse(content); json.name = newName; const newContent = await stringifyJson(json); await writeFile(brunoJsonFilePath, newContent); // todo: listen for bruno.json changes and handle it in watcher // the app will change the name of the collection after parsing the bruno.json file contents mainWindow.webContents.send('main:collection-renamed', { collectionPathname, newName }); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:save-folder-root', async (event, folder) => { try { const { name: folderName, root: folderRoot, pathname: folderPathname } = folder; const folderBruFilePath = path.join(folderPathname, 'folder.bru'); folderRoot.meta = { name: folderName }; const content = await jsonToCollectionBru( folderRoot, true // isFolder ); await writeFile(folderBruFilePath, content); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:save-collection-root', async (event, collectionPathname, collectionRoot) => { try { const collectionBruFilePath = path.join(collectionPathname, 'collection.bru'); const content = await jsonToCollectionBru(collectionRoot); await writeFile(collectionBruFilePath, content); } catch (error) { return Promise.reject(error); } }); // new request ipcMain.handle('renderer:new-request', async (event, pathname, request) => { try { if (fs.existsSync(pathname)) { throw new Error(`path: ${pathname} already exists`); } // For the actual filename part, we want to be strict if (!validateName(request?.filename)) { throw new Error(`${request.filename}.bru is not a valid filename`); } const content = await jsonToBruViaWorker(request); await writeFile(pathname, content); } catch (error) { return Promise.reject(error); } }); // save request ipcMain.handle('renderer:save-request', async (event, pathname, request) => { try { if (!fs.existsSync(pathname)) { throw new Error(`path: ${pathname} does not exist`); } const content = await jsonToBruViaWorker(request); await writeFile(pathname, content); } catch (error) { return Promise.reject(error); } }); // save multiple requests ipcMain.handle('renderer:save-multiple-requests', async (event, requestsToSave) => { try { for (let r of requestsToSave) { const request = r.item; const pathname = r.pathname; if (!fs.existsSync(pathname)) { throw new Error(`path: ${pathname} does not exist`); } const content = await jsonToBruViaWorker(request); await writeFile(pathname, content); } } catch (error) { return Promise.reject(error); } }); // create environment ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => { try { const envDirPath = path.join(collectionPathname, 'environments'); if (!fs.existsSync(envDirPath)) { await createDirectory(envDirPath); } const envFilePath = path.join(envDirPath, `${name}.bru`); if (fs.existsSync(envFilePath)) { throw new Error(`environment: ${envFilePath} already exists`); } const environment = { name: name, variables: variables || [] }; if (envHasSecrets(environment)) { environmentSecretsStore.storeEnvSecrets(collectionPathname, environment); } const content = await envJsonToBru(environment); await writeFile(envFilePath, content); } catch (error) { return Promise.reject(error); } }); // save environment ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environment) => { try { const envDirPath = path.join(collectionPathname, 'environments'); if (!fs.existsSync(envDirPath)) { await createDirectory(envDirPath); } const envFilePath = path.join(envDirPath, `${environment.name}.bru`); if (!fs.existsSync(envFilePath)) { throw new Error(`environment: ${envFilePath} does not exist`); } if (envHasSecrets(environment)) { environmentSecretsStore.storeEnvSecrets(collectionPathname, environment); } const content = await envJsonToBru(environment); await writeFile(envFilePath, content); } catch (error) { return Promise.reject(error); } }); // rename environment ipcMain.handle('renderer:rename-environment', async (event, collectionPathname, environmentName, newName) => { try { const envDirPath = path.join(collectionPathname, 'environments'); const envFilePath = path.join(envDirPath, `${environmentName}.bru`); if (!fs.existsSync(envFilePath)) { throw new Error(`environment: ${envFilePath} does not exist`); } const newEnvFilePath = path.join(envDirPath, `${newName}.bru`); if (!safeToRename(envFilePath, newEnvFilePath)) { throw new Error(`environment: ${newEnvFilePath} already exists`); } fs.renameSync(envFilePath, newEnvFilePath); environmentSecretsStore.renameEnvironment(collectionPathname, environmentName, newName); } catch (error) { return Promise.reject(error); } }); // delete environment ipcMain.handle('renderer:delete-environment', async (event, collectionPathname, environmentName) => { try { const envDirPath = path.join(collectionPathname, 'environments'); const envFilePath = path.join(envDirPath, `${environmentName}.bru`); if (!fs.existsSync(envFilePath)) { throw new Error(`environment: ${envFilePath} does not exist`); } fs.unlinkSync(envFilePath); environmentSecretsStore.deleteEnvironment(collectionPathname, environmentName); } catch (error) { return Promise.reject(error); } }); // rename item ipcMain.handle('renderer:rename-item-name', async (event, { itemPath, newName }) => { try { if (!fs.existsSync(itemPath)) { throw new Error(`path: ${itemPath} does not exist`); } if (isDirectory(itemPath)) { const folderBruFilePath = path.join(itemPath, 'folder.bru'); let folderBruFileJsonContent; if (fs.existsSync(folderBruFilePath)) { const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); } else { folderBruFileJsonContent = {}; } folderBruFileJsonContent.meta = { name: newName, }; const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); await writeFile(folderBruFilePath, folderBruFileContent); return; } const isBru = hasBruExtension(itemPath); if (!isBru) { throw new Error(`path: ${itemPath} is not a bru file`); } const data = fs.readFileSync(itemPath, 'utf8'); const jsonData = await bruToJson(data); jsonData.name = newName; const content = await jsonToBru(jsonData); await writeFile(itemPath, content); } catch (error) { return Promise.reject(error); } }); // rename item ipcMain.handle('renderer:rename-item-filename', async (event, { oldPath, newPath, newName, newFilename }) => { const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`); const isWindowsOSAndNotWSLPathAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath); try { // Check if the old path exists if (!fs.existsSync(oldPath)) { throw new Error(`path: ${oldPath} does not exist`); } if (!safeToRename(oldPath, newPath)) { throw new Error(`path: ${newPath} already exists`); } if (isDirectory(oldPath)) { const folderBruFilePath = path.join(oldPath, 'folder.bru'); let folderBruFileJsonContent; if (fs.existsSync(folderBruFilePath)) { const oldFolderBruFileContent = await fs.promises.readFile(folderBruFilePath, 'utf8'); folderBruFileJsonContent = await collectionBruToJson(oldFolderBruFileContent); } else { folderBruFileJsonContent = {}; } folderBruFileJsonContent.meta = { name: newName, }; const folderBruFileContent = await jsonToCollectionBru(folderBruFileJsonContent, true); await writeFile(folderBruFilePath, folderBruFileContent); const bruFilesAtSource = await searchForBruFiles(oldPath); for (let bruFile of bruFilesAtSource) { const newBruFilePath = bruFile.replace(oldPath, newPath); moveRequestUid(bruFile, newBruFilePath); } /** * If it is windows OS * And it is not a WSL path (meaning it is not running in WSL (linux pathtype)) * And it has sub directories * Only then we need to use the temp dir approach to rename the folder * * Windows OS would sometimes throw error when renaming a folder with sub directories * This is an alternative approach to avoid that error */ if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) { await fsExtra.copy(oldPath, tempDir); await fsExtra.remove(oldPath); await fsExtra.move(tempDir, newPath, { overwrite: true }); await fsExtra.remove(tempDir); } else { await fs.renameSync(oldPath, newPath); } return newPath; } if (!hasBruExtension(oldPath)) { throw new Error(`path: ${oldPath} is not a bru file`); } if (!validateName(newFilename)) { throw new Error(`path: ${newFilename} is not a valid filename`); } // 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); jsonData.name = newName; moveRequestUid(oldPath, newPath); const content = await jsonToBruViaWorker(jsonData); await fs.promises.unlink(oldPath); await writeFile(newPath, content); return newPath; } catch (error) { // in case the rename file operations fails, and we see that the temp dir exists // and the old path does not exist, we need to restore the data from the temp dir to the old path if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) { if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) { try { await fsExtra.copy(tempDir, oldPath); await fsExtra.remove(tempDir); } catch (err) { console.error("Failed to restore data to the old path:", err); } } } return Promise.reject(error); } }); // new folder ipcMain.handle('renderer:new-folder', async (event, pathname, folderName) => { const resolvedFolderName = sanitizeName(path.basename(pathname)); pathname = path.join(path.dirname(pathname), resolvedFolderName); try { if (!fs.existsSync(pathname)) { fs.mkdirSync(pathname); const folderBruFilePath = path.join(pathname, 'folder.bru'); let data = { meta: { name: folderName, } }; const content = await jsonToCollectionBru(data, true); // isFolder flag await writeFile(folderBruFilePath, content); } else { return Promise.reject(new Error('The directory already exists')); } } catch (error) { return Promise.reject(error); } }); // delete file/folder ipcMain.handle('renderer:delete-item', async (event, pathname, type) => { try { if (type === 'folder') { if (!fs.existsSync(pathname)) { return Promise.reject(new Error('The directory does not exist')); } // delete the request uid mappings const bruFilesAtSource = await searchForBruFiles(pathname); for (let bruFile of bruFilesAtSource) { deleteRequestUid(bruFile); } fs.rmSync(pathname, { recursive: true, force: true }); } else if (['http-request', 'graphql-request'].includes(type)) { if (!fs.existsSync(pathname)) { return Promise.reject(new Error('The file does not exist')); } deleteRequestUid(pathname); fs.unlinkSync(pathname); } else { return Promise.reject(); } } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:open-collection', () => { if (watcher && mainWindow) { openCollectionDialog(mainWindow, watcher); } }); ipcMain.handle('renderer:remove-collection', async (event, collectionPath) => { if (watcher && mainWindow) { console.log(`watcher stopWatching: ${collectionPath}`); watcher.removeWatcher(collectionPath, mainWindow); lastOpenedCollections.remove(collectionPath); } }); ipcMain.handle('renderer:update-collection-paths', async (_, collectionPaths) => { lastOpenedCollections.update(collectionPaths); }) ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => { try { let collectionName = sanitizeName(collection.name); let collectionPath = path.join(collectionLocation, collectionName); if (fs.existsSync(collectionPath)) { throw new Error(`collection: ${collectionPath} already exists`); } // Recursive function to parse the collection items and create files/folders const parseCollectionItems = (items = [], currentPath) => { 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 filePath = path.join(currentPath, sanitizedFilename); safeWriteFileSync(filePath, content); } if (item.type === 'folder') { let sanitizedFolderName = sanitizeName(item?.filename || item?.name); const folderPath = path.join(currentPath, sanitizedFolderName); fs.mkdirSync(folderPath); if (item?.root?.meta?.name) { const folderBruFilePath = path.join(folderPath, 'folder.bru'); const folderContent = await jsonToCollectionBru( item.root, true // isFolder ); safeWriteFileSync(folderBruFilePath, folderContent); } if (item.items && item.items.length) { parseCollectionItems(item.items, folderPath); } } // Handle items of type 'js' if (item.type === 'js') { let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.js`); const filePath = path.join(currentPath, sanitizedFilename); safeWriteFileSync(filePath, item.fileContent); } }); }; const parseEnvironments = (environments = [], collectionPath) => { const envDirPath = path.join(collectionPath, 'environments'); if (!fs.existsSync(envDirPath)) { fs.mkdirSync(envDirPath); } environments.forEach(async (env) => { const content = await envJsonToBru(env); let sanitizedEnvFilename = sanitizeName(`${env.name}.bru`); const filePath = path.join(envDirPath, sanitizedEnvFilename); safeWriteFileSync(filePath, content); }); }; const getBrunoJsonConfig = (collection) => { let brunoConfig = collection.brunoConfig; if (!brunoConfig) { brunoConfig = { version: '1', name: collection.name, type: 'collection', ignore: ['node_modules', '.git'] }; } return brunoConfig; }; await createDirectory(collectionPath); const uid = generateUidBasedOnHash(collectionPath); let brunoConfig = getBrunoJsonConfig(collection); const stringifiedBrunoConfig = await stringifyJson(brunoConfig); // Write the Bruno configuration to a file await writeFile(path.join(collectionPath, 'bruno.json'), stringifiedBrunoConfig); const collectionContent = await jsonToCollectionBru(collection.root); await writeFile(path.join(collectionPath, 'collection.bru'), collectionContent); const { size, filesCount } = await getCollectionStats(collectionPath); brunoConfig.size = size; brunoConfig.filesCount = filesCount; mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig); ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid, brunoConfig); lastOpenedCollections.add(collectionPath); // create folder and files based on collection await parseCollectionItems(collection.items, collectionPath); await parseEnvironments(collection.environments, collectionPath); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:clone-folder', async (event, itemFolder, collectionPath) => { try { if (fs.existsSync(collectionPath)) { throw new Error(`folder: ${collectionPath} already exists`); } // Recursive function to parse the folder and create files/folders const parseCollectionItems = (items = [], currentPath) => { items.forEach(async (item) => { if (['http-request', 'graphql-request'].includes(item.type)) { const content = await jsonToBruViaWorker(item); const filePath = path.join(currentPath, item.filename); safeWriteFileSync(filePath, content); } if (item.type === 'folder') { const folderPath = path.join(currentPath, item.filename); fs.mkdirSync(folderPath); // If folder has a root element, then I should write its folder.bru file if (item.root) { const folderContent = await jsonToCollectionBru(item.root, true); folderContent.name = item.name; if (folderContent) { const bruFolderPath = path.join(folderPath, `folder.bru`); safeWriteFileSync(bruFolderPath, folderContent); } } if (item.items && item.items.length) { parseCollectionItems(item.items, folderPath); } } }); }; await createDirectory(collectionPath); // 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); if (folderContent) { const bruFolderPath = path.join(collectionPath, `folder.bru`); safeWriteFileSync(bruFolderPath, folderContent); } } // create folder and files based on another folder await parseCollectionItems(itemFolder.items, collectionPath); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => { try { for await (let item of itemsToResequence) { const bru = fs.readFileSync(item.pathname, 'utf8'); const jsonData = await bruToJsonViaWorker(bru); if (jsonData.seq !== item.seq) { jsonData.seq = item.seq; const content = await jsonToBruViaWorker(jsonData); await writeFile(item.pathname, content); } } } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:move-file-item', async (event, itemPath, destinationPath) => { try { const itemContent = fs.readFileSync(itemPath, 'utf8'); const newItemPath = path.join(destinationPath, path.basename(itemPath)); moveRequestUid(itemPath, newItemPath); fs.unlinkSync(itemPath); safeWriteFileSync(newItemPath, itemContent); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:move-folder-item', async (event, folderPath, destinationPath) => { try { const folderName = path.basename(folderPath); const newFolderPath = path.join(destinationPath, folderName); if (!fs.existsSync(folderPath)) { throw new Error(`folder: ${folderPath} does not exist`); } if (fs.existsSync(newFolderPath)) { throw new Error(`folder: ${newFolderPath} already exists`); } const bruFilesAtSource = await searchForBruFiles(folderPath); for (let bruFile of bruFilesAtSource) { const newBruFilePath = bruFile.replace(folderPath, newFolderPath); moveRequestUid(bruFile, newBruFilePath); } fs.renameSync(folderPath, newFolderPath); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:update-bruno-config', async (event, brunoConfig, collectionPath, collectionUid) => { try { const brunoConfigPath = path.join(collectionPath, 'bruno.json'); const content = await stringifyJson(brunoConfig); await writeFile(brunoConfigPath, content); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:open-devtools', async () => { mainWindow.webContents.openDevTools(); }); ipcMain.handle('renderer:load-gql-schema-file', async () => { try { const { filePaths } = await dialog.showOpenDialog(mainWindow, { properties: ['openFile'] }); if (filePaths.length === 0) { return; } const jsonData = fs.readFileSync(filePaths[0], 'utf8'); return safeParseJSON(jsonData); } catch (err) { return Promise.reject(new Error('Failed to load GraphQL schema file')); } }); ipcMain.handle('renderer:delete-cookies-for-domain', async (event, domain) => { try { await deleteCookiesForDomain(domain); const domainsWithCookies = await getDomainsWithCookies(); mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:delete-cookie', async (event, domain, path, cookieKey) => { try { await deleteCookie(domain, path, cookieKey); const domainsWithCookies = await getDomainsWithCookies(); mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); } catch (error) { return Promise.reject(error); } }); // add cookie ipcMain.handle('renderer:add-cookie', async (event, domain, cookie) => { try { await addCookieForDomain(domain, cookie); const domainsWithCookies = await getDomainsWithCookies(); mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); } catch (error) { return Promise.reject(error); } }); // modify cookie ipcMain.handle('renderer:modify-cookie', async (event, domain, oldCookie, cookie) => { try { await modifyCookieForDomain(domain, oldCookie, cookie); const domainsWithCookies = await getDomainsWithCookies(); mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:get-parsed-cookie', async (event, cookieStr) => { try { return parseCookieString(cookieStr); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:create-cookie-string', async (event, cookie) => { try { return createCookieString(cookie); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:save-collection-security-config', async (event, collectionPath, securityConfig) => { try { collectionSecurityStore.setSecurityConfigForCollection(collectionPath, { jsSandboxMode: securityConfig.jsSandboxMode }); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:get-collection-security-config', async (event, collectionPath) => { try { return collectionSecurityStore.getSecurityConfigForCollection(collectionPath); } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:update-ui-state-snapshot', (event, { type, data }) => { try { uiStateSnapshotStore.update({ type, data }); } catch (error) { throw new Error(error.message); } }); ipcMain.handle('renderer:get-stored-oauth2-credentials', async (event, collectionUid, url, credentialsId) => { try { const oauth2Store = new Oauth2Store(); const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId }); return { credentials, collectionUid, url }; } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:fetch-oauth2-credentials', async (event, { itemUid, request, collection }) => { try { if (request.oauth2) { let requestCopy = _.cloneDeep(request); const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection; const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid); const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); const partialItem = { uid: itemUid }; const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem); if (requestTreePath && requestTreePath.length > 0) { mergeVars(collection, requestCopy, requestTreePath); } interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); const {newRequest} = await configureRequestWithCertsAndProxy({ collectionUid, request: requestCopy, envVars, runtimeVariables, processEnvVars, collectionPath }); requestCopy = newRequest const { oauth2: { grantType }} = requestCopy || {}; let credentials, url, credentialsId; switch (grantType) { case 'authorization_code': interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); ({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, forceFetch: true })); break; case 'client_credentials': interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); ({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, forceFetch: true })); break; case 'password': interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); ({ credentials, url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, forceFetch: true })); break; } return { credentials, url, collectionUid, credentialsId, debugInfo }; } } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:load-request-via-worker', async (event, { collectionUid, pathname }) => { let fileStats; try { fileStats = fs.statSync(pathname); if (hasBruExtension(pathname)) { const file = { meta: { collectionUid, pathname, name: path.basename(pathname) } }; let bruContent = fs.readFileSync(pathname, 'utf8'); const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); 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.partial = false; file.loading = true; file.size = sizeInMB(fileStats?.size); hydrateRequestWithUuid(file.data, pathname); mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); } } catch (error) { if (hasBruExtension(pathname)) { const file = { meta: { collectionUid, pathname, name: path.basename(pathname) } }; let bruContent = fs.readFileSync(pathname, 'utf8'); const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); file.data = metaJson; file.partial = true; file.loading = false; file.size = sizeInMB(fileStats?.size); hydrateRequestWithUuid(file.data, pathname); mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); } return Promise.reject(error); } }); ipcMain.handle('renderer:refresh-oauth2-credentials', async (event, { request, collection }) => { try { if (request.oauth2) { let requestCopy = _.cloneDeep(request); const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection; const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid); const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars); const {newRequest} = await configureRequestWithCertsAndProxy({ collectionUid, request: requestCopy, envVars, runtimeVariables, processEnvVars, collectionPath }); requestCopy = newRequest let { credentials, url, credentialsId } = await refreshOauth2Token(requestCopy, collectionUid); return { credentials, url, collectionUid, credentialsId }; } } catch (error) { return Promise.reject(error); } }); ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => { let fileStats; try { fileStats = fs.statSync(pathname); if (hasBruExtension(pathname)) { const file = { meta: { collectionUid, pathname, name: path.basename(pathname) } }; let bruContent = fs.readFileSync(pathname, 'utf8'); const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); 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.partial = false; file.loading = true; file.size = sizeInMB(fileStats?.size); hydrateRequestWithUuid(file.data, pathname); mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); } } catch (error) { if (hasBruExtension(pathname)) { const file = { meta: { collectionUid, pathname, name: path.basename(pathname) } }; let bruContent = fs.readFileSync(pathname, 'utf8'); const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); file.data = metaJson; file.partial = true; file.loading = false; file.size = sizeInMB(fileStats?.size); hydrateRequestWithUuid(file.data, pathname); mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); } return Promise.reject(error); } }); ipcMain.handle('renderer:mount-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => { const { size, filesCount, maxFileSize } = await getCollectionStats(collectionPathname); const shouldLoadCollectionAsync = (size > MAX_COLLECTION_SIZE_IN_MB) || (filesCount > MAX_COLLECTION_FILES_COUNT) || (maxFileSize > MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB); watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync); }); ipcMain.handle('renderer:show-in-folder', async (event, filePath) => { try { if (!filePath) { throw new Error('File path is required'); } shell.showItemInFolder(filePath); } catch (error) { console.error('Error in show-in-folder: ', error); throw error; } }); }; const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { ipcMain.on('main:open-collection', () => { if (watcher && mainWindow) { openCollectionDialog(mainWindow, watcher); } }); ipcMain.on('main:open-docs', () => { const docsURL = 'https://docs.usebruno.com'; shell.openExternal(docsURL); }); ipcMain.on('main:collection-opened', async (win, pathname, uid, brunoConfig) => { lastOpenedCollections.add(pathname); app.addRecentDocument(pathname); }); // The app listen for this event and allows the user to save unsaved requests before closing the app ipcMain.on('main:start-quit-flow', () => { mainWindow.webContents.send('main:start-quit-flow'); }); ipcMain.handle('main:complete-quit-flow', () => { mainWindow.destroy(); }); ipcMain.handle('main:force-quit', () => { process.exit(); }); }; const registerCollectionsIpc = (mainWindow, watcher, lastOpenedCollections) => { registerRendererEventHandlers(mainWindow, watcher, lastOpenedCollections); registerMainEventHandlers(mainWindow, watcher, lastOpenedCollections); }; module.exports = registerCollectionsIpc;