@@ -141,9 +168,14 @@ const Headers = ({ collection }) => {
: null}
-
+
+
+
+
-
+ |
formik.setFieldValue(`${index}.value`, newValue, true)}
/>
+ {!variable.secret && hasSensitiveUsage(variable.name) && (
+
+ )}
|
e.header);
const Headers = ({ collection, folder }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = get(folder, 'root.request.headers', []);
+ const [isBulkEditMode, setIsBulkEditMode] = useState(false);
+
+ const toggleBulkEditMode = () => {
+ setIsBulkEditMode(!isBulkEditMode);
+ };
+
+ const handleBulkHeadersChange = (newHeaders) => {
+ dispatch(setFolderHeaders({ collectionUid: collection.uid, folderUid: folder.uid, headers: newHeaders }));
+ };
const addHeader = () => {
dispatch(
@@ -62,6 +72,22 @@ const Headers = ({ collection, folder }) => {
);
};
+ if (isBulkEditMode) {
+ return (
+
+
+ Request headers that will be sent with every request inside this folder.
+
+
+
+ );
+ }
+
return (
@@ -141,9 +167,14 @@ const Headers = ({ collection, folder }) => {
: null}
-
+
+
+
+
-
+
{
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..e872894d8 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,7 +66,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 d61bbf013..207b3510b 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';
@@ -10,13 +11,14 @@ import { inputsConfig } from './inputsConfig';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
import AdditionalParams from '../AdditionalParams/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,
@@ -133,12 +135,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)}
@@ -147,6 +152,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 6ca38c47c..3ad22ae28 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';
@@ -10,13 +11,14 @@ import Dropdown from 'components/Dropdown';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
import AdditionalParams from '../AdditionalParams/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 {
@@ -99,12 +101,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)}
@@ -113,6 +118,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 ae530bccb..43bb35cbd 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';
@@ -10,14 +11,15 @@ import Dropdown from 'components/Dropdown';
import Oauth2TokenViewer from '../Oauth2TokenViewer/index';
import Oauth2ActionButtons from '../Oauth2ActionButtons/index';
import AdditionalParams from '../AdditionalParams/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,
@@ -102,12 +104,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)}
@@ -116,6 +121,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/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 && (
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..aa3a3c9f0
--- /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 {
+ const allRequestUidsOrder = flattenedRequests.map(item => item.uid);
+
+ if (selectedItems.length === flattenedRequests.length) {
+ setSelectedItems([]);
+ dispatch(updateRunnerConfiguration(collection.uid, [], allRequestUidsOrder));
+ } else {
+ setSelectedItems(allRequestUidsOrder);
+ dispatch(updateRunnerConfiguration(collection.uid, allRequestUidsOrder, 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 f2dbe9d7f..897d9d241 100644
--- a/packages/bruno-app/src/components/RunnerResults/index.jsx
+++ b/packages/bruno-app/src/components/RunnerResults/index.jsx
@@ -2,15 +2,17 @@ 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, 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 } 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';
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', {});
@@ -103,18 +123,39 @@ 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 = () => {
- 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
)
);
};
@@ -125,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);
@@ -142,59 +196,104 @@ export default function RunnerResults({ collection }) {
if (!items || !items.length) {
return (
-
-
- Runner
-
-
-
- You have {totalRequestsInCollection} requests in this collection.
-
- {isCollectionLoading ? Requests in this collection are still loading. : null}
-
-
- setDelay(e.target.value)}
- />
-
+
+
+
+
+ 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 */}
-
+ {/* Tags for the collection run */}
+
-
+ {/* Configure requests option */}
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+ {configureMode && (
+
+
+
+ )}
+
);
}
return (
-
-
-
+
+
+
Runner
{runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid && (
-
-
+
+
@@ -214,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}
-
+
{runnerInfo.status === 'ended' ? (
@@ -366,15 +469,15 @@ export default function RunnerResults({ collection }) {
{selectedItem.displayName}
- {allTestsPassed(selectedItem) ?
+ {allTestsPassed(selectedItem) ?
- : null}
- {anyTestFailed(selectedItem) ?
-
- : null}
+ : null}
+ {anyTestFailed(selectedItem) ?
+
+ : null}
{selectedItem.status === 'skipped' ?
- : null}
+ : null}
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/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/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/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/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 = () => {
-
- setCookiesOpen(true)}
- tabIndex={0}
- aria-label="Open Cookies Settings"
- >
-
-
-
-
@@ -92,7 +80,20 @@ const StatusBar = () => {
-
+
+ setCookiesOpen(true)}
+ tabIndex={0}
+ aria-label="Open Cookies"
+ >
+
+
+ Cookies
+
+
+
0 ? 'has-errors' : ''}`}
data-trigger="dev-tools"
@@ -100,13 +101,13 @@ const StatusBar = () => {
tabIndex={0}
aria-label={`Open Dev Tools${errorCount > 0 ? ` (${errorCount} errors)` : ''}`}
>
-
-
- Dev Tools
- {errorCount > 0 && (
- {errorCount}
- )}
-
+
+
+ Dev Tools
+ {errorCount > 0 && (
+ {errorCount}
+ )}
+
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;
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 (
-
+
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
+ };
+};
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/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
+ })
+ );
}
});
}
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-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index 2df8ee03b..25cdbb93c 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -38,7 +38,8 @@ import {
setCollectionSecurityConfig,
collectionAddOauth2CredentialsByUrl,
collectionClearOauth2CredentialsByUrl,
- initRunRequestEvent
+ initRunRequestEvent,
+ updateRunnerConfiguration as _updateRunnerConfiguration
} from './index';
import { each } from 'lodash';
@@ -316,9 +317,9 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
};
-export const runCollectionFolder = (collectionUid, folderUid, recursive, delay, tags) => (dispatch, getState) => {
+export const runCollectionFolder = (collectionUid, folderUid, recursive, delay, tags, selectedRequestUids) => (dispatch, getState) => {
const state = getState();
- const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
+ const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;
const collection = findCollectionByUid(state.collections.collections, collectionUid);
return new Promise((resolve, reject) => {
@@ -346,6 +347,26 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive, delay,
})
);
+ // to only include those requests in the specified order while preserving folder data
+ if (selectedRequestUids && selectedRequestUids.length > 0) {
+ const newItems = [];
+
+ selectedRequestUids.forEach((uid, index) => {
+ const requestItem = findItemInCollection(collectionCopy, uid);
+ if (requestItem) {
+ const clonedRequest = cloneDeep(requestItem);
+ clonedRequest.seq = index + 1;
+ newItems.push(clonedRequest);
+ }
+ });
+
+ if (folder) {
+ folder.items = newItems;
+ } else {
+ collectionCopy.items = newItems;
+ }
+ }
+
const { ipcRenderer } = window;
ipcRenderer
.invoke(
@@ -1064,7 +1085,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 }));
})
@@ -1373,3 +1394,11 @@ export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig
ipcRenderer.invoke('renderer:show-in-folder', collectionPath).then(resolve).catch(reject);
});
};
+
+export const updateRunnerConfiguration = (collectionUid, selectedRequestItems, requestItemsOrder) => (dispatch) => {
+ dispatch(_updateRunnerConfiguration({
+ collectionUid,
+ selectedRequestItems,
+ requestItemsOrder
+ }));
+};
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..3cd033124 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) {
@@ -867,6 +873,43 @@ export const collectionsSlice = createSlice({
enabled: enabled
}));
},
+ setCollectionHeaders: (state, action) => {
+ const { collectionUid, headers } = action.payload;
+
+ const collection = findCollectionByUid(state.collections, collectionUid);
+ if (!collection) {
+ return;
+ }
+
+ collection.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
+ uid: uuid(),
+ name: name,
+ value: value,
+ description: '',
+ enabled: enabled
+ }));
+ },
+ setFolderHeaders: (state, action) => {
+ const { collectionUid, folderUid, headers } = action.payload;
+
+ const collection = findCollectionByUid(state.collections, collectionUid);
+ if (!collection) {
+ return;
+ }
+
+ const folder = findItemInCollection(collection, folderUid);
+ if (!folder || !isItemAFolder(folder)) {
+ return;
+ }
+
+ folder.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
+ uid: uuid(),
+ name: name,
+ value: value,
+ description: '',
+ enabled: enabled
+ }));
+ },
addFormUrlEncodedParam: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -2232,6 +2275,7 @@ export const collectionsSlice = createSlice({
collection.runnerResult = null;
collection.runnerTags = { include: [], exclude: [] }
collection.runnerTagsEnabled = false;
+ collection.runnerConfiguration = null;
}
},
updateRunnerTagsDetails: (state, action) => {
@@ -2246,6 +2290,16 @@ export const collectionsSlice = createSlice({
}
}
},
+ updateRunnerConfiguration: (state, action) => {
+ const { collectionUid, selectedRequestItems, requestItemsOrder } = action.payload;
+ const collection = findCollectionByUid(state.collections, collectionUid);
+ if (collection) {
+ collection.runnerConfiguration = {
+ selectedRequestItems: selectedRequestItems || [],
+ requestItemsOrder: requestItemsOrder || []
+ };
+ }
+ },
updateRequestDocs: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
@@ -2413,6 +2467,7 @@ export const collectionsSlice = createSlice({
export const {
createCollection,
updateCollectionMountStatus,
+ updateCollectionLoadingState,
setCollectionSecurityConfig,
brunoConfigUpdateEvent,
renameCollection,
@@ -2454,6 +2509,8 @@ export const {
deleteRequestHeader,
moveRequestHeader,
setRequestHeaders,
+ setCollectionHeaders,
+ setFolderHeaders,
addFormUrlEncodedParam,
updateFormUrlEncodedParam,
deleteFormUrlEncodedParam,
@@ -2516,6 +2573,7 @@ export const {
runFolderEvent,
resetCollectionRunner,
updateRunnerTagsDetails,
+ updateRunnerConfiguration,
updateRequestDocs,
updateFolderDocs,
moveCollection,
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',
diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js
index 7b65f4234..c2d1ba9ba 100644
--- a/packages/bruno-app/src/utils/codemirror/autocomplete.js
+++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js
@@ -37,6 +37,7 @@ const STATIC_API_HINTS = {
'res.headers',
'res.body',
'res.responseTime',
+ 'res.url',
'res.getStatus()',
'res.getStatusText()',
'res.getHeader(name)',
@@ -48,6 +49,7 @@ const STATIC_API_HINTS = {
'res.getSize().header',
'res.getSize().body',
'res.getSize().total',
+ 'res.getUrl()'
],
bru: [
'bru',
@@ -78,7 +80,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-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 35366c00b..5de3a4dd2 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-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);
diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json
index e1b74e191..c298273b4 100644
--- a/packages/bruno-cli/package.json
+++ b/packages/bruno-cli/package.json
@@ -48,11 +48,12 @@
"dependencies": {
"@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/vm2": "^3.9.13",
"@usebruno/requests": "^0.1.0",
- "@usebruno/converters": "^0.1.0",
+ "@usebruno/vm2": "^3.9.13",
"aws4-axios": "^3.3.0",
"axios": "^1.8.3",
"axios-ntlm": "^1.4.2",
@@ -68,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/commands/run.js b/packages/bruno-cli/src/commands/run.js
index 811314d41..e3b7aa3ea 100644
--- a/packages/bruno-cli/src/commands/run.js
+++ b/packages/bruno-cli/src/commands/run.js
@@ -5,15 +5,16 @@ 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 { hasExecutableTestInScript } = require('../utils/request');
const command = 'run [paths...]';
const desc = 'Run one or more requests/folders';
@@ -346,7 +347,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 +440,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;
@@ -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/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index e8ecb4d61..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})(:?\/\/|:)/;
@@ -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');
@@ -459,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!',
@@ -598,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,
@@ -626,6 +632,7 @@ const runSingleRequest = async function (
statusText: null,
headers: null,
data: null,
+ url: null,
responseTime: 0
},
status: 'error',
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..79218eb50 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, stringifyRequest } = 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-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-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
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/package.json b/packages/bruno-converters/package.json
index b942dea98..2a2450ef0 100644
--- a/packages/bruno-converters/package.json
+++ b/packages/bruno-converters/package.json
@@ -35,13 +35,13 @@
"@web/rollup-plugin-copy": "^0.5.1",
"babel-jest": "^29.7.0",
"rimraf": "^5.0.7",
- "rollup": "3.2.5",
+ "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.2.5"
+ "rollup": "3.29.5"
}
}
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 d7303e2fb..0a2c5af89 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -32,12 +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",
"about-window": "^1.15.2",
"aws4-axios": "^3.3.0",
"axios": "^1.8.3",
@@ -64,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"
},
@@ -72,7 +72,7 @@
"dmg-license": "^1.0.11"
},
"devDependencies": {
- "electron": "33.2.1",
+ "electron": "~37.2.6",
"electron-builder": "25.1.8",
"electron-devtools-installer": "^4.0.0"
}
diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js
index d6b6cbfba..acdb8873b 100644
--- a/packages/bruno-electron/src/app/collection-watcher.js
+++ b/packages/bruno-electron/src/app/collection-watcher.js
@@ -3,12 +3,18 @@ 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');
-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');
@@ -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);
@@ -92,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);
}
};
@@ -115,7 +122,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()));
@@ -126,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;
}
});
}
@@ -160,7 +168,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)) {
@@ -177,7 +185,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 +217,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 +241,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);
@@ -245,6 +253,8 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
}
if (hasBruExtension(pathname)) {
+ watcher.addFileToProcessing(collectionUid, pathname);
+
const file = {
meta: {
collectionUid,
@@ -258,14 +268,17 @@ 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);
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;
}
@@ -278,7 +291,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 +308,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);
@@ -314,6 +327,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);
}
}
};
@@ -331,7 +346,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 +385,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 +417,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 +440,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 +462,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 +505,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;
}
@@ -504,7 +519,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);
@@ -514,6 +532,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) {
@@ -521,6 +608,10 @@ class CollectionWatcher {
this.watchers[watchPath].close();
}
+ this.initializeLoadingState(collectionUid);
+
+ this.startCollectionDiscovery(win, collectionUid);
+
const ignores = brunoConfig?.ignore || [];
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
@@ -546,8 +637,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))
@@ -582,11 +673,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/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..8ca5ac2f9 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'));
@@ -585,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);
}
});
@@ -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/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 078cc20c4..2369f698e 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 {
@@ -901,6 +910,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) {
@@ -1093,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;
}
@@ -1208,13 +1221,14 @@ 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
});
} catch (error) {
// Skip further processing if request was cancelled
if (axios.isCancel(error)) {
- throw Promise.reject(error);
+ throw error;
}
if (error?.response) {
@@ -1248,7 +1262,7 @@ const registerNetworkIpc = (mainWindow) => {
await executeRequestOnFailHandler(request, error);
// if it's not a network error, don't continue
- throw Promise.reject(error);
+ throw error;
}
}
@@ -1280,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;
}
@@ -1384,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/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();
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/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);
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/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-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/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');
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-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();
+ });
+});
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..edf79b6f1
--- /dev/null
+++ b/packages/bruno-filestore/package.json
@@ -0,0 +1,47 @@
+{
+ "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",
+ "@types/node": "^24.1.0",
+ "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..2caf3ac4f
--- /dev/null
+++ b/packages/bruno-filestore/src/index.ts
@@ -0,0 +1,100 @@
+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;
+
+const getWorkerInstance = (): BruParserWorker => {
+ if (!globalWorkerInstance) {
+ globalWorkerInstance = new BruParserWorker();
+ }
+ 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..bb248ee3f
--- /dev/null
+++ b/packages/bruno-filestore/src/workers/WorkerQueue/index.ts
@@ -0,0 +1,114 @@
+import { Worker } from 'node: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..af66ea107
--- /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 'node: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..7a6529aab
--- /dev/null
+++ b/packages/bruno-filestore/src/workers/worker-script.ts
@@ -0,0 +1,27 @@
+import { parentPort } from 'node: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..22385164b
--- /dev/null
+++ b/packages/bruno-filestore/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "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,
+ "types": ["node"],
+ "lib": ["ES2020"],
+ "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/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/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/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-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-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 655714f13..58667a485 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -73,10 +73,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
@@ -333,10 +332,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
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
+}
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..8329c120b
--- /dev/null
+++ b/packages/bruno-tests/collection/scripting/api/res/getUrl.bru
@@ -0,0 +1,27 @@
+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.url", function() {
+ expect(res.url).to.equal("https://testbench-sanity.usebruno.com/ping");
+ });
+
+ test("res.getUrl()", function() {
+ const url = res.getUrl();
+ expect(url).to.equal("https://testbench-sanity.usebruno.com/ping");
+ });
+}
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',
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(
|