Compare commits

...

14 Commits

Author SHA1 Message Date
Anoop M D
a18e5e5b94 Feat/safe mode quickjs (#2825)
* feat: testing quickjs sanbox

* feat: testing quickjs sanbox

* feat: testing quickjs sanbox
2024-08-13 19:06:17 +05:30
lohit
bde2c57a23 feat: safe mode updates (#2813)
* feat: safe mode updates
* feat: safe-mode updates
2024-08-12 16:52:59 +05:30
Anoop M D
9955b8796e feat: safe mode updates 2024-08-12 14:10:52 +05:30
Anoop M D
3b3fa8a856 feat: safe mode updates 2024-08-12 13:10:21 +05:30
Anoop M D
c687856a9e feat: safe mode updates 2024-08-12 12:28:41 +05:30
Anoop M D
033bba1805 feat: safe mode updates 2024-08-12 12:27:32 +05:30
Anoop M D
4fcb6f0980 feat: safe mode updates 2024-08-11 23:14:08 +05:30
Anoop M D
e4d2e5c1cf feat: safe mode updates 2024-08-11 19:22:48 +05:30
Anoop M D
969fe8e3cf feat: safe mode updates 2024-08-11 18:19:15 +05:30
Anoop M D
6d01c46d50 feat: safe mode updates 2024-08-11 18:02:05 +05:30
Anoop M D
751c7aa16d feat: safe mode updates 2024-08-11 16:11:12 +05:30
Anoop M D
b2baa1e48d feat: safe mode updates 2024-08-10 21:59:13 +05:30
Anoop M D
f834eb4425 feat: safe mode updates 2024-08-10 21:30:50 +05:30
lohit
26f8dd7a7b Safe Mode using isolated-vm (#2511)
feat: safe mode using isolated vm
2024-08-10 16:05:57 +05:30
88 changed files with 9897 additions and 19635 deletions

View File

@@ -26,7 +26,18 @@ jobs:
npm run build --workspace=packages/bruno-common
npm run build --workspace=packages/bruno-query
# test
# rebuild isolated-vm for bruno-js
- name: Rebuild bruno-js isolated-vm
run: |
cd packages/bruno-js/node_modules/isolated-vm
npm run rebuild || true
# tests
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
- name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Test Package bruno-lang
@@ -35,12 +46,8 @@ jobs:
run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-common
run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
- name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron
@@ -63,6 +70,12 @@ jobs:
npm run build --workspace=packages/bruno-query
npm run build --workspace=packages/bruno-common
# rebuild isolated-vm for bruno-js
- name: Rebuild bruno-js isolated-vm
run: |
cd packages/bruno-js/node_modules/isolated-vm
npm run rebuild || true
- name: Run tests
run: |
cd packages/bruno-tests/collection

View File

@@ -57,6 +57,9 @@ npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
# run next app (terminal 1)
npm run dev:web

26448
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,12 +47,14 @@
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"prepare": "husky install"
},
"overrides": {
"rollup": "3.2.5"
},
"dependencies": {
"isolated-vm": "^5.0.0",
"json-bigint": "^1.0.0",
"lossless-json": "^4.0.1"
"lossless-json": "^4.0.1",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0"
}
}

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import StyledWrapper from './StyledWrapper';
const ModalHeader = ({ title, handleCancel, customHeader }) => (
const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
<div className="bruno-modal-header">
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
{handleCancel ? (
{handleCancel && !hideClose ? (
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
×
</div>
@@ -63,6 +63,7 @@ const Modal = ({
confirmDisabled,
hideCancel,
hideFooter,
hideClose,
disableCloseOnOutsideClick,
disableEscapeKey,
onClick,
@@ -100,7 +101,12 @@ const Modal = ({
return (
<StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}>
<div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} customHeader={customHeader} />
<ModalHeader
title={title}
hideClose={hideClose}
handleCancel={() => closeModal({ type: 'icon' })}
customHeader={customHeader}
/>
<ModalContent>{children}</ModalContent>
<ModalFooter
confirmText={confirmText}

View File

@@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
import SecuritySettings from 'components/SecuritySettings/index';
import FolderSettings from 'components/FolderSettings';
const MIN_LEFT_PANE_WIDTH = 300;
@@ -137,6 +138,10 @@ const RequestTabPanel = () => {
return <FolderSettings collection={collection} folder={folder} />;
}
if (focusedTab.type === 'security-settings') {
return <SecuritySettings collection={collection} />;
}
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;

View File

@@ -5,6 +5,7 @@ import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode';
const CollectionToolBar = ({ collection }) => {
const dispatch = useDispatch();
@@ -47,6 +48,9 @@ const CollectionToolBar = ({ collection }) => {
<span className="ml-2 mr-4 font-semibold">{collection?.name}</span>
</div>
<div className="flex flex-1 items-center justify-end">
<span className="mr-2">
<JsSandboxMode collection={collection} />
</span>
<span className="mr-2">
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
</span>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { IconVariable, IconSettings, IconRun, IconFolder } from '@tabler/icons';
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
const getTabInfo = (type, tabName) => {
@@ -12,6 +12,14 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
</>
);
}
case 'security-settings': {
return (
<>
<IconShieldLock size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Security</span>
</>
)
}
case 'folder-settings': {
return (
<div className="flex items-center flex-nowrap overflow-hidden">

View File

@@ -81,7 +81,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
return color;
};
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner'].includes(tab.type)) {
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
{tab.type === 'folder-settings' ? (

View File

@@ -3,6 +3,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
position: absolute;
height: 100%;
width: calc(100% - 0.75rem);
z-index: 1;
background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg};

View File

@@ -13,7 +13,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
};
return (
<StyledWrapper className="px-3 w-full">
<StyledWrapper className="w-full">
<div className="overlay">
<div style={{ marginBottom: 15, fontSize: 26 }}>
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.safe-mode {
padding: 0.15rem 0.3rem;
color: ${(props) => props.theme.colors.text.green};
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
}
.developer-mode {
padding: 0.15rem 0.3rem;
color: ${(props) => props.theme.colors.text.yellow};
border: solid 1px ${(props) => props.theme.colors.text.yellow} !important;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,45 @@
import { useDispatch } from 'react-redux';
import { IconShieldLock } from '@tabler/icons';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { uuid } from 'utils/common/index';
import JsSandboxModeModal from '../JsSandboxModeModal';
import StyledWrapper from './StyledWrapper';
const JsSandboxMode = ({ collection }) => {
const jsSandboxMode = collection?.securityConfig?.jsSandboxMode;
const dispatch = useDispatch();
const viewSecuritySettings = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'security-settings'
})
);
};
return (
<StyledWrapper className='flex'>
{jsSandboxMode === 'safe' && (
<div
className="flex items-center border rounded-md text-xs cursor-pointer safe-mode"
onClick={viewSecuritySettings}
>
Safe Mode
</div>
)}
{jsSandboxMode === 'developer' && (
<div
className="flex items-center border rounded-md text-xs cursor-pointer developer-mode"
onClick={viewSecuritySettings}
>
Developer Mode
</div>
)}
{!jsSandboxMode ? <JsSandboxModeModal collection={collection} /> : null}
</StyledWrapper>
);
};
export default JsSandboxMode;

View File

@@ -0,0 +1,22 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
span.beta-tag {
display: flex;
align-items: center;
padding: 0.1rem 0.25rem;
font-size: 0.75rem;
border-radius: 0.25rem;
color: ${(props) => props.theme.colors.text.green};
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
}
span.developer-mode-warning {
font-weight: 400;
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,99 @@
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import { useState } from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import StyledWrapper from './StyledWrapper';
const JsSandboxModeModal = ({ collection, onClose }) => {
const dispatch = useDispatch();
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
const handleChange = (e) => {
setJsSandboxMode(e.target.value);
};
const handleSave = () => {
dispatch(
saveCollectionSecurityConfig(collection?.uid, {
jsSandboxMode: jsSandboxMode
})
)
.then(() => {
toast.success('Sandbox mode updated successfully');
onClose();
})
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
};
return (
<Portal>
<Modal
size="sm"
title={'JavaScript Sandbox'}
confirmText="Save"
handleConfirm={handleSave}
hideCancel={true}
hideClose={true}
disableCloseOnOutsideClick={true}
disableEscapeKey={true}
>
<StyledWrapper>
<div>
The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
</div>
<div className='text-muted mt-6'>
Please choose the security level for the JavaScript code execution.
</div>
<div className="flex flex-col mt-4">
<label htmlFor="safe" className="flex flex-row items-center gap-2 cursor-pointer">
<input
type="radio"
id="safe"
name="jsSandboxMode"
value="safe"
checked={jsSandboxMode === 'safe'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
Safe Mode
</span>
<span className='beta-tag'>BETA</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code is executed in a secure sandbox and cannot excess your filesystem or execute system commands.
</p>
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">
<input
type="radio"
id="developer"
name="jsSandboxMode"
value="developer"
checked={jsSandboxMode === 'developer'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'developer' ? 'font-medium' : 'font-normal'}>
Developer Mode
<span className='ml-1 developer-mode-warning'>(use only if you trust the collections authors)</span>
</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
</p>
<small className='text-muted mt-6'>
* SAFE mode has been introduced v1.25 onwards and is in beta. Please report any issues on github.
</small>
</div>
</StyledWrapper>
</Modal>
</Portal>
);
};
export default JsSandboxModeModal;

View File

@@ -0,0 +1,22 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
span.beta-tag {
display: flex;
align-items: center;
padding: 0.1rem 0.25rem;
font-size: 0.75rem;
border-radius: 0.25rem;
color: ${(props) => props.theme.colors.text.green};
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
}
span.developer-mode-warning {
font-weight: 400;
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,86 @@
import { useState } from 'react';
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import { useDispatch } from 'react-redux';
const SecuritySettings = ({ collection }) => {
const dispatch = useDispatch();
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
const handleChange = (e) => {
setJsSandboxMode(e.target.value);
};
const handleSave = () => {
dispatch(
saveCollectionSecurityConfig(collection?.uid, {
jsSandboxMode: jsSandboxMode
})
)
.then(() => {
toast.success('Sandbox mode updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
};
return (
<StyledWrapper className="flex flex-col h-full relative px-4 py-4">
<div className='font-semibold mt-2'>JavaScript Sandbox</div>
<div className='mt-4'>
The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
</div>
<div className="flex flex-col mt-4">
<div className="flex flex-col">
<label htmlFor="safe" className="flex flex-row items-center gap-2 cursor-pointer">
<input
type="radio"
id="safe"
name="jsSandboxMode"
value="safe"
checked={jsSandboxMode === 'safe'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
Safe Mode
</span>
<span className='beta-tag'>BETA</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code is executed in a secure sandbox and cannot excess your filesystem or execute system commands.
</p>
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">
<input
type="radio"
id="developer"
name="jsSandboxMode"
value="developer"
checked={jsSandboxMode === 'developer'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'developer' ? 'font-medium' : 'font-normal'}>
Developer Mode
<span className='ml-1 developer-mode-warning'>(use only if you trust the collections authors)</span>
</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
</p>
</div>
<button onClick={handleSave} className="submit btn btn-sm btn-secondary w-fit mt-6">
Save
</button>
<small className='text-muted mt-6'>
* SAFE mode has been introduced v1.25 onwards and is in beta. Please report any issues on github.
</small>
</div>
</StyledWrapper>
);
};
export default SecuritySettings;

View File

@@ -115,7 +115,7 @@ const Collections = () => {
)}
</div>
<div className="mt-4 flex flex-col overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
<div className="mt-4 flex flex-col overflow-hidden hover:overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
{collections && collections.length
? collections.map((c) => {
return (

View File

@@ -33,7 +33,8 @@ import {
requestCancelled,
resetRunResults,
responseReceived,
updateLastAction
updateLastAction,
setCollectionSecurityConfig
} from './index';
import { each } from 'lodash';
@@ -1039,11 +1040,13 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
};
return new Promise((resolve, reject) => {
collectionSchema
.validate(collection)
.then(() => dispatch(_createCollection(collection)))
.then(resolve)
.catch(reject);
ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => {
collectionSchema
.validate(collection)
.then(() => dispatch(_createCollection({ ...collection, securityConfig })))
.then(resolve)
.catch(reject);
});
});
};
@@ -1108,3 +1111,19 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g
ipcRenderer.invoke('renderer:import-collection', collection, collectionLocation).then(resolve).catch(reject);
});
};
export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
ipcRenderer
.invoke('renderer:save-collection-security-config', collection?.pathname, securityConfig)
.then(async () => {
await dispatch(setCollectionSecurityConfig({ collectionUid, securityConfig }));
resolve();
})
.catch(reject);
});
};

View File

@@ -33,7 +33,6 @@ export const collectionsSlice = createSlice({
const collection = action.payload;
collection.settingsSelectedTab = 'headers';
collection.folderLevelSettingsSelectedTab = {};
// TODO: move this to use the nextAction approach
@@ -51,6 +50,12 @@ export const collectionsSlice = createSlice({
state.collections.push(collection);
}
},
setCollectionSecurityConfig: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
collection.securityConfig = action.payload.securityConfig;
}
},
brunoConfigUpdateEvent: (state, action) => {
const { collectionUid, brunoConfig } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -1622,6 +1627,7 @@ export const collectionsSlice = createSlice({
export const {
createCollection,
setCollectionSecurityConfig,
brunoConfigUpdateEvent,
renameCollection,
removeCollection,

View File

@@ -24,7 +24,9 @@ export const tabsSlice = createSlice({
return;
}
if (['variables', 'collection-settings', 'collection-runner'].includes(action.payload.type)) {
if (
['variables', 'collection-settings', 'collection-runner', 'security-settings'].includes(action.payload.type)
) {
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
if (tab) {
state.activeTabUid = tab.uid;

View File

@@ -39,6 +39,11 @@ const runSingleRequest = async function (
const scriptingConfig = get(brunoConfig, 'scripts', {});
// todo: allow to override from cli args
// we will default to vm2 (developer-mode) for 1.x version for backward compatibility
// 2.x will default to isolated-vm (safe mode)
scriptingConfig.runtime = 'isolated-vm';
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
@@ -57,7 +62,7 @@ const runSingleRequest = async function (
// run pre-request vars
const preRequestVars = get(bruJson, 'request.vars.req');
if (preRequestVars?.length) {
const varsRuntime = new VarsRuntime();
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPreRequestVars(
preRequestVars,
request,
@@ -74,7 +79,7 @@ const runSingleRequest = async function (
get(bruJson, 'request.script.req')
]).join(os.EOL);
if (requestScriptFile?.length) {
const scriptRuntime = new ScriptRuntime();
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
const result = await scriptRuntime.runRequestScript(
decomment(requestScriptFile),
request,
@@ -276,7 +281,7 @@ const runSingleRequest = async function (
// run post-response vars
const postResponseVars = get(bruJson, 'request.vars.res');
if (postResponseVars?.length) {
const varsRuntime = new VarsRuntime();
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPostResponseVars(
postResponseVars,
request,
@@ -294,7 +299,7 @@ const runSingleRequest = async function (
get(bruJson, 'request.script.res')
]).join(os.EOL);
if (responseScriptFile?.length) {
const scriptRuntime = new ScriptRuntime();
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
const result = await scriptRuntime.runResponseScript(
decomment(responseScriptFile),
request,
@@ -315,7 +320,7 @@ const runSingleRequest = async function (
let assertionResults = [];
const assertions = get(bruJson, 'request.assertions');
if (assertions) {
const assertRuntime = new AssertRuntime();
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
assertionResults = assertRuntime.runAssertions(
assertions,
request,
@@ -339,7 +344,7 @@ const runSingleRequest = async function (
let testResults = [];
const testFile = compact([get(collectionRoot, 'request.tests'), get(bruJson, 'request.tests')]).join(os.EOL);
if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const result = await testRuntime.runTests(
decomment(testFile),
request,

View File

@@ -8,7 +8,7 @@
"author": "Anoop M D <anoop.md1421@gmail.com> (https://helloanoop.com/)",
"scripts": {
"clean": "rimraf dist",
"dev": "electron .",
"dev": "electron --no-node-snapshot .",
"dist:mac": "electron-builder --mac --config electron-builder-config.js",
"dist:win": "electron-builder --win --config electron-builder-config.js",
"dist:linux": "electron-builder --linux AppImage --config electron-builder-config.js",
@@ -64,7 +64,7 @@
"dmg-license": "^1.0.11"
},
"devDependencies": {
"electron": "31.2.1",
"electron": "23.3.3",
"electron-builder": "23.0.2",
"electron-icon-maker": "^0.0.5"
}

View File

@@ -20,8 +20,10 @@ const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON
const { moveRequestUid, deleteRequestUid } = require('../cache/requestUids');
const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cookies');
const EnvironmentSecretsStore = require('../store/env-secrets');
const CollectionSecurityStore = require('../store/collection-security');
const environmentSecretsStore = new EnvironmentSecretsStore();
const collectionSecurityStore = new CollectionSecurityStore();
const envHasSecrets = (environment = {}) => {
const secrets = _.filter(environment.variables, (v) => v.secret);
@@ -665,6 +667,24 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return Promise.reject(error);
}
});
ipcMain.handle('renderer:save-collection-security-config', async (event, collectionPath, securityConfig) => {
try {
collectionSecurityStore.setSecurityConfigForCollection(collectionPath, {
jsSandboxMode: securityConfig.jsSandboxMode
});
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:get-collection-security-config', async (event, collectionPath) => {
try {
return collectionSecurityStore.getSecurityConfigForCollection(collectionPath);
} catch (error) {
return Promise.reject(error);
}
});
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {

View File

@@ -81,6 +81,11 @@ const getEnvVars = (environment = {}) => {
};
};
const getJsSandboxRuntime = (collection) => {
const securityConfig = get(collection, 'securityConfig', {});
return securityConfig.jsSandboxMode === 'safe' ? 'quickjs' : 'vm2';
};
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const configureRequest = async (
@@ -315,7 +320,7 @@ const registerNetworkIpc = (mainWindow) => {
// run pre-request vars
const preRequestVars = get(request, 'vars.req', []);
if (preRequestVars?.length) {
const varsRuntime = new VarsRuntime();
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
varsRuntime.runPreRequestVars(
preRequestVars,
request,
@@ -330,7 +335,7 @@ const registerNetworkIpc = (mainWindow) => {
let scriptResult;
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
if (requestScript?.length) {
const scriptRuntime = new ScriptRuntime();
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
scriptResult = await scriptRuntime.runRequestScript(
decomment(requestScript),
request,
@@ -382,7 +387,7 @@ const registerNetworkIpc = (mainWindow) => {
// run post-response vars
const postResponseVars = get(request, 'vars.res', []);
if (postResponseVars?.length) {
const varsRuntime = new VarsRuntime();
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
const result = varsRuntime.runPostResponseVars(
postResponseVars,
request,
@@ -416,7 +421,7 @@ const registerNetworkIpc = (mainWindow) => {
let scriptResult;
if (responseScript?.length) {
const scriptRuntime = new ScriptRuntime();
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
scriptResult = await scriptRuntime.runResponseScript(
decomment(responseScript),
request,
@@ -460,6 +465,7 @@ const registerNetworkIpc = (mainWindow) => {
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
try {
const controller = new AbortController();
@@ -575,7 +581,7 @@ const registerNetworkIpc = (mainWindow) => {
// run assertions
const assertions = get(request, 'assertions');
if (assertions) {
const assertRuntime = new AssertRuntime();
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
const results = assertRuntime.runAssertions(
assertions,
request,
@@ -603,7 +609,7 @@ const registerNetworkIpc = (mainWindow) => {
]).join(os.EOL);
if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const testResults = await testRuntime.runTests(
decomment(testFile),
request,
@@ -661,6 +667,7 @@ const registerNetworkIpc = (mainWindow) => {
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
await runPreRequest(
request,
@@ -766,6 +773,7 @@ const registerNetworkIpc = (mainWindow) => {
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collection.uid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
await runPreRequest(
request,
@@ -832,6 +840,7 @@ const registerNetworkIpc = (mainWindow) => {
const cancelTokenUid = uuid();
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
const collectionRoot = get(collection, 'root', {});
const abortController = new AbortController();
@@ -1028,7 +1037,7 @@ const registerNetworkIpc = (mainWindow) => {
// run assertions
const assertions = get(item, 'request.assertions');
if (assertions) {
const assertRuntime = new AssertRuntime();
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
const results = assertRuntime.runAssertions(
assertions,
request,
@@ -1055,7 +1064,7 @@ const registerNetworkIpc = (mainWindow) => {
]).join(os.EOL);
if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const testResults = await testRuntime.runTests(
decomment(testFile),
request,

View File

@@ -0,0 +1,39 @@
const _ = require('lodash');
const Store = require('electron-store');
class CollectionSecurityStore {
constructor() {
this.store = new Store({
name: 'collection-security',
clearInvalidConfig: true
});
}
setSecurityConfigForCollection(collectionPathname, securityConfig) {
const collections = this.store.get('collections') || [];
const collection = _.find(collections, (c) => c.path === collectionPathname);
if (!collection) {
collections.push({
path: collectionPathname,
securityConfig: {
jsSandboxMode: securityConfig.jsSandboxMode
}
});
this.store.set('collections', collections);
return;
}
collection.securityConfig = securityConfig || {};
this.store.set('collections', collections);
}
getSecurityConfigForCollection(collectionPathname) {
const collections = this.store.get('collections') || [];
const collection = _.find(collections, (c) => c.path === collectionPathname);
return collection?.securityConfig || {};
}
}
module.exports = CollectionSecurityStore;

View File

@@ -86,7 +86,11 @@ function decryptString(str) {
}
if (algo === ELECTRONSAFESTORAGE_ALGO) {
return safeStorageDecrypt(encryptedString);
if (safeStorage && safeStorage.isEncryptionAvailable()) {
return safeStorageDecrypt(encryptedString);
} else {
return '';
}
}
if (algo === AES256_ALGO) {

1
packages/bruno-js/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
src/sandbox/bundle-browser-rollup.js

View File

@@ -11,7 +11,11 @@
"@n8n/vm2": "^3.9.23"
},
"scripts": {
"test": "jest --testPathIgnorePatterns test.js"
"test": "jest --testPathIgnorePatterns test.js",
"sandbox:bundle-libraries": "node ./src/sandbox/bundle-libraries.js",
"isolated-vm:install": "cd ./node_modules/isolated-vm && npm install",
"isolated-vm:prebuild:dev": "node ./scripts/prebuild-isolated-vm-for-dev.js",
"isolated-vm:prebuild:prod": "node ./scripts/prebuild-isolated-vm-for-prod-builds.js"
},
"dependencies": {
"@usebruno/common": "0.1.0",
@@ -24,12 +28,28 @@
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"crypto-js": "^4.1.1",
"isolated-vm": "4.6.0",
"json-query": "^2.2.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "3.3.4",
"node-fetch": "2.*",
"node-vault": "^0.10.2",
"quickjs-emscripten": "^0.29.2",
"uuid": "^9.0.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"rollup": "3.2.5",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-polyfill-node": "^0.13.0",
"rollup-plugin-terser": "^7.0.2",
"stream": "^0.0.2",
"terser": "^5.31.1",
"uglify-js": "^3.18.0",
"util": "^0.12.5"
}
}

View File

@@ -0,0 +1,11 @@
const { execSync } = require('child_process');
const os = require('os');
const platform = os.platform();
const arch = os.arch();
const target = '23.3.3';
const command = `cd ./node_modules/isolated-vm && rm -rf prebuilds && mkdir prebuilds && npm run prebuild -- --platform=${platform} --arch=${arch} --target=${target} --runtime=electron`;
console.log(`Running command: ${command}`);
execSync(command, { stdio: 'inherit' });

View File

@@ -0,0 +1,21 @@
const { execSync } = require('child_process');
const darwin_x64 = `cd ./node_modules/isolated-vm && npm run prebuild -- --platform=darwin --arch=x64 --target=21.1.1 --runtime=electron`;
const darwin_arm64 = `cd ./node_modules/isolated-vm && npm run prebuild -- --platform=darwin --arch=arm64 --target=21.1.1 --runtime=electron`;
const linux_x64 = `cd ./node_modules/isolated-vm && npm run prebuild -- --platform=linux --arch=x64 --target=21.1.1 --runtime=electron`;
const linux_arm64 = `cd ./node_modules/isolated-vm && npm run prebuild -- --platform=linux --arch=arm64 --target=21.1.1 --runtime=electron`;
const win32_x64 = `cd ./node_modules/isolated-vm && npm run prebuild -- --platform=win32 --arch=x64 --target=21.1.1 --runtime=electron`;
const win32_arm64 = `cd ./node_modules/isolated-vm && npm run prebuild -- --platform=win32 --arch=arm64 --target=21.1.1 --runtime=electron`;
console.log(`Running command: ${darwin_x64}`);
execSync(darwin_x64, { stdio: 'inherit' });
console.log(`Running command: ${darwin_arm64}`);
execSync(darwin_arm64, { stdio: 'inherit' });
console.log(`Running command: ${linux_x64}`);
execSync(linux_x64, { stdio: 'inherit' });
console.log(`Running command: ${linux_arm64}`);
execSync(linux_arm64, { stdio: 'inherit' });
console.log(`Running command: ${win32_x64}`);
execSync(win32_x64, { stdio: 'inherit' });
console.log(`Running command: ${win32_arm64}`);
execSync(win32_arm64, { stdio: 'inherit' });

View File

@@ -5,6 +5,7 @@ const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { interpolateString } = require('../interpolate-string');
const { executeInIsolatedVMStrict } = require('../sandbox/isolatedvm');
const { expect } = chai;
chai.use(require('chai-string'));
@@ -161,7 +162,31 @@ const isUnaryOperator = (operator) => {
return unaryOperators.includes(operator);
};
const evaluateRhsOperand = (rhsOperand, operator, context) => {
const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
if(runtime === 'isolated-vm') {
return executeInIsolatedVMStrict({
script: literal,
context,
scriptType: 'template-literal'
});
}
return evaluateJsTemplateLiteral(literal, context);
};
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime) => {
if(runtime === 'isolated-vm') {
return executeInIsolatedVMStrict({
script: expr,
context,
scriptType: 'expression'
});
}
return evaluateJsExpression(expr, context);
}
const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
if (isUnaryOperator(operator)) {
return;
}
@@ -181,13 +206,25 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
return rhsOperand
.split(',')
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
.map((v) =>
evaluateJsTemplateLiteralBasedOnRuntime(
interpolateString(v.trim(), interpolationContext),
context,
runtime
)
);
}
if (operator === 'between') {
const [lhs, rhs] = rhsOperand
.split(',')
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
.map((v) =>
evaluateJsTemplateLiteralBasedOnRuntime(
interpolateString(v.trim(), interpolationContext),
context,
runtime
)
);
return [lhs, rhs];
}
@@ -200,10 +237,18 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
return interpolateString(rhsOperand, interpolationContext);
}
return evaluateJsTemplateLiteral(interpolateString(rhsOperand, interpolationContext), context);
return evaluateJsTemplateLiteralBasedOnRuntime(
interpolateString(rhsOperand, interpolationContext),
context,
runtime
);
};
class AssertRuntime {
constructor(props) {
this.runtime = props?.runtime || 'vm2';
}
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
const requestVariables = request?.requestVariables || {};
const enabledAssertions = _.filter(assertions, (a) => a.enabled);
@@ -238,8 +283,8 @@ class AssertRuntime {
const { operator, value: rhsOperand } = parseAssertionOperator(rhsExpr);
try {
const lhs = evaluateJsExpression(lhsExpr, context);
const rhs = evaluateRhsOperand(rhsOperand, operator, context);
const lhs = evaluateJsExpressionBasedOnRuntime(lhsExpr, context, this.runtime);
const rhs = evaluateRhsOperand(rhsOperand, operator, context, this.runtime);
switch (operator) {
case 'eq':

View File

@@ -28,9 +28,12 @@ const fetch = require('node-fetch');
const chai = require('chai');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
const { executeInIsolatedVMAsync } = require('../sandbox/isolatedvm');
class ScriptRuntime {
constructor() {}
constructor(props) {
this.runtime = props?.runtime || 'vm2';
}
// This approach is getting out of hand
// Need to refactor this to use a single arg (object) instead of 7
@@ -86,6 +89,23 @@ class ScriptRuntime {
};
}
if (this.runtime === 'isolated-vm') {
await executeInIsolatedVMAsync({
script: script,
context: context,
modules: {},
scriptType: 'jsScript'
});
return {
request,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
nextRequestName: bru.nextRequest
};
}
// default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {
@@ -123,6 +143,7 @@ class ScriptRuntime {
});
const asyncVM = vm.run(`module.exports = async () => { ${script} }`, path.join(collectionPath, 'vm.js'));
await asyncVM();
return {
request,
envVariables: cleanJson(envVariables),
@@ -176,10 +197,28 @@ class ScriptRuntime {
log: customLogger('log'),
info: customLogger('info'),
warn: customLogger('warn'),
error: customLogger('error')
error: customLogger('error'),
debug: customLogger('debug')
};
}
if (this.runtime === 'isolated-vm') {
await executeInIsolatedVMAsync({
script: script,
context: context,
modules: {},
scriptType: 'jsScript'
});
return {
response,
envVariables: cleanJson(envVariables),
runtimeVariables: cleanJson(runtimeVariables),
nextRequestName: bru.nextRequest
};
}
// default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {

View File

@@ -30,9 +30,13 @@ const axios = require('axios');
const fetch = require('node-fetch');
const CryptoJS = require('crypto-js');
const NodeVault = require('node-vault');
const { executeInIsolatedVMAsync } = require('../sandbox/isolatedvm');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
class TestRuntime {
constructor() {}
constructor(props) {
this.runtime = props?.runtime || 'vm2';
}
async runTests(
testsFile,
@@ -105,44 +109,58 @@ class TestRuntime {
};
}
const vm = new NodeVM({
sandbox: context,
require: {
context: 'sandbox',
external: true,
root: [collectionPath, ...additionalContextRootsAbsolute],
mock: {
// node libs
path,
stream,
util,
url,
http,
https,
punycode,
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
btoa,
atob,
lodash,
moment,
uuid,
nanoid,
axios,
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
if (this.runtime === 'isolated-vm') {
await executeInIsolatedVMAsync({
script: testsFile,
context: context,
modules: {},
scriptType: 'jsScript'
});
} else if(this.runtime === 'quickjs') {
await executeQuickJsVmAsync({
script: testsFile,
context: context
});
} else {
// default runtime is vm2
const vm = new NodeVM({
sandbox: context,
require: {
context: 'sandbox',
external: true,
root: [collectionPath, ...additionalContextRootsAbsolute],
mock: {
// node libs
path,
stream,
util,
url,
http,
https,
punycode,
zlib,
// 3rd party libs
ajv,
'ajv-formats': addFormats,
btoa,
atob,
lodash,
moment,
uuid,
nanoid,
axios,
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
}
}
}
});
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
await asyncVM();
});
const asyncVM = vm.run(`module.exports = async () => { ${testsFile}}`, path.join(collectionPath, 'vm.js'));
await asyncVM();
}
return {
request,

View File

@@ -3,7 +3,38 @@ const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { executeInIsolatedVMStrict } = require('../sandbox/isolatedvm');
const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
if(runtime === 'isolated-vm') {
return executeInIsolatedVMStrict({
script: literal,
context,
scriptType: 'template-literal'
});
}
return evaluateJsTemplateLiteral(literal, context);
};
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
if(runtime === 'isolated-vm') {
return executeInIsolatedVMStrict({
script: expr,
context,
scriptType: 'expression'
});
}
return evaluateJsExpression(expr, context);
};
class VarsRuntime {
constructor(props) {
this.runtime = props?.runtime || 'vm2';
this.mode = props?.mode || 'developer';
}
runPreRequestVars(vars, request, envVariables, runtimeVariables, collectionPath, processEnvVars) {
if (!request?.requestVariables) {
request.requestVariables = {};
@@ -28,7 +59,7 @@ class VarsRuntime {
};
_.each(enabledVars, (v) => {
const value = evaluateJsTemplateLiteral(v.value, context);
const value = evaluateJsTemplateLiteralBasedOnRuntime(v.value, context, this.runtime);
request?.requestVariables && (request.requestVariables[v.name] = value);
});
}
@@ -59,7 +90,7 @@ class VarsRuntime {
const errors = new Map();
_.each(enabledVars, (v) => {
try {
const value = evaluateJsExpression(v.value, context);
const value = evaluateJsExpressionBasedOnRuntime(v.value, context, this.runtime);
bru.setVar(v.name, value);
} catch (error) {
errors.set(v.name, error);

View File

@@ -0,0 +1,84 @@
const rollup = require('rollup');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const fs = require('fs');
const { terser } = require('rollup-plugin-terser');
const bundleLibraries = async () => {
const codeScript = `
import { expect, assert } from 'chai';
import { Buffer } from "buffer";
import moment from "moment";
import btoa from "btoa";
import atob from "atob";
globalThis.expect = expect;
globalThis.assert = assert;
globalThis.moment = moment;
globalThis.btoa = btoa;
globalThis.atob = atob;
globalThis.Buffer = Buffer;
globalThis.requireObject = {
'chai': { expect, assert },
'moment': moment,
'buffer': { Buffer },
'btoa': btoa,
'atob': atob,
};
`;
const config = {
input: {
input: 'inline-code',
plugins: [
{
name: 'inline-code-plugin',
resolveId(id) {
if (id === 'inline-code') {
return id;
}
return null;
},
load(id) {
if (id === 'inline-code') {
return codeScript;
}
return null;
}
},
nodeResolve({
preferBuiltins: false,
browser: false
}),
commonjs(),
terser()
]
},
output: {
file: './src/sandbox/bundle-browser-rollup.js',
format: 'iife',
name: 'MyBundle'
}
};
try {
const bundle = await rollup.rollup(config.input);
const { output } = await bundle.generate(config.output);
fs.writeFileSync(
'./src/sandbox/bundle-browser-rollup.js',
`
const getBundledCode = () => {
return function(){
${output?.map((o) => o.code).join('\n')}
}()
}
module.exports = getBundledCode;
`
);
} catch (error) {
console.error('Error while bundling:', error);
}
};
bundleLibraries();
module.exports = bundleLibraries;

View File

@@ -0,0 +1,167 @@
const ivm = require('isolated-vm');
const addBruShimToContext = require('./shims/bru');
const addBrunoRequestShimToContext = require('./shims/bruno-request');
const addConsoleShimToContext = require('./shims/console');
const addBrunoResponseShimToContext = require('./shims/bruno-response');
const addTestShimToContext = require('./shims/test');
const addLibraryShimsToContext = require('./shims/lib');
// execute `npm run build:isolated-vm:inbuilt-modules` if the below file doesn't exist
const getBundledCode = require('../bundle-browser-rollup');
const addSleepShimToContext = require('./shims/sleep');
const toNumber = (value) => {
const num = Number(value);
return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);
};
const executeInIsolatedVMStrict = ({ script: externalScript, context: externalContext, scriptType = 'script' }) => {
if (!isNaN(Number(externalScript))) {
return Number(externalScript);
}
let result;
const isolate = new ivm.Isolate();
try {
const context = isolate.createContextSync();
context.global.setSync('global', context.global.derefInto());
const { bru, req, res } = externalContext;
context.evalSync(`
let bru = {};
let req = {};
let res = {};
`);
bru && addBruShimToContext(context, bru);
req && addBrunoRequestShimToContext(context, req);
res && addBrunoResponseShimToContext(context, res);
context.global.setSync('setResult', function (arg) {
result = arg;
});
const templateLiteralText = `
let value = \`${externalScript}\`;
setResult(value);
`;
const jsExpressionText = `
let value = ${externalScript};
setResult(value);
`;
let scriptText = scriptType === 'template-literal' ? templateLiteralText : jsExpressionText;
const script = isolate.compileScriptSync(scriptText);
script.runSync(context);
return result;
} catch (error) {
console.error('Error executing the script!', error);
}
isolate.dispose();
};
const executeInIsolatedVMAsync = async ({
script: externalScript,
context: externalContext,
modules = {},
scriptType = 'script'
}) => {
if (!isNaN(Number(externalScript))) {
return toNumber(externalScript);
}
let result;
const isolate = new ivm.Isolate();
try {
const context = await isolate.createContext();
await context.global.set('global', context.global.derefInto());
context.evalSync(`
let bru = {};
let req = {};
let res = {};
let console = {};
global.requireObject = {};
`);
context.global.setSync('log', function (...args) {
console.debug(...args);
});
try {
const bundledCode = getBundledCode?.toString() || '';
await context.eval(`(${bundledCode})()`);
} catch (err) {
console.debug('Error bundling libraries', err);
}
const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext;
bru && addBruShimToContext(context, bru);
req && addBrunoRequestShimToContext(context, req);
res && addBrunoResponseShimToContext(context, res);
consoleFn && addConsoleShimToContext(context, consoleFn);
addSleepShimToContext(context);
await context.eval(
`
global.require = (module) => {
return global.requireObject[module];
}
`
);
await addLibraryShimsToContext(context);
test && __brunoTestResults && (await addTestShimToContext(context, __brunoTestResults));
context.global.setSync('setResult', function (arg) {
result = arg;
});
const jsScriptText = `
new Promise(async (resolve, reject) => {
// modify the setTimeout function with the shim to work-around the callback-function clone issues
setTimeout = global.setTimeout;
console?.debug && console.debug('isolated-vm:execution-start:');
try {
${externalScript}
} catch (error) {
console?.debug && console.debug('isolated-vm:execution-end:with-error', error?.message);
}
console?.debug && console.debug('isolated-vm:execution-end:');
resolve();
});
`;
const templateLiteralText = `
let value = \`${externalScript}\`;
setResult(value);
`;
const jsExpressionText = `
let value = ${externalScript};
setResult(value);
`;
let scriptText =
scriptType === 'template-literal'
? templateLiteralText
: scriptType === 'expression'
? jsExpressionText
: jsScriptText;
const script = await isolate.compileScript(scriptText);
await script.run(context);
return result;
} catch (error) {
console.error('Error executing the script!', error);
}
isolate.dispose();
};
module.exports = {
executeInIsolatedVMStrict,
executeInIsolatedVMAsync
};

View File

@@ -0,0 +1,59 @@
const addBruShimToContext = (context, bru) => {
context.global.setSync('cwd', function () {
return bru.cwd();
});
context.global.setSync('getEnvName', function () {
return bru.getEnvName();
});
context.global.setSync('getProcessEnv', function (key) {
return bru.getProcessEnv(key);
});
context.global.setSync('getEnvVar', function (key) {
return bru.getEnvVar(key);
});
context.global.setSync('setEnvVar', function (key, value) {
bru.setEnvVar(key, value);
});
context.global.setSync('setVar', function (key, value) {
bru.setVar(key, value);
});
context.global.setSync('getVar', function (key) {
return bru.getVar(key);
});
context.global.setSync('setNextRequest', function (nextRequest) {
bru.setNextRequest(nextRequest);
});
context.global.setSync('visualize', function (htmlString) {
bru.visualize(htmlString);
});
context.global.setSync('getSecretVar', function (key) {
return bru.getSecretVar(key);
});
context.evalSync(`
bru = {
...bru || {},
cwd: global.cwd,
getEnvName: global.getEnvName,
getProcessEnv: global.getProcessEnv,
getEnvVar: global.getEnvVar,
setEnvVar: global.setEnvVar,
setVar: global.setVar,
getVar: global.getVar,
setNextRequest: global.setNextRequest,
visualize: global.visualize,
getSecretVar: global.getSecretVar
}
`);
};
module.exports = addBruShimToContext;

View File

@@ -0,0 +1,79 @@
const addBrunoRequestShimToContext = (context, req) => {
context.global.setSync('getUrl', function () {
return req.getUrl();
});
context.global.setSync('setUrl', function (url) {
req.setUrl(url);
});
context.global.setSync('getMethod', function () {
return req.getMethod();
});
context.global.setSync('getAuthMode', function () {
return req.getAuthMode();
});
context.global.setSync('setMethod', function (method) {
req.setMethod(method);
});
context.global.setSync('getHeaders', function () {
return req.getHeaders();
});
context.global.setSync('setHeaders', function (headers) {
req.setHeaders(headers);
});
context.global.setSync('getHeader', function (name) {
return req.getHeader(name);
});
context.global.setSync('setHeader', function (name, value) {
req.setHeader(name, value);
});
context.global.setSync('getBody', function () {
return req.getBody();
});
context.global.setSync('setBody', function (data) {
req.setBody(data);
});
context.global.setSync('setMaxRedirects', function (maxRedirects) {
req.setMaxRedirects(maxRedirects);
});
context.global.setSync('getTimeout', function () {
return req.getTimeout();
});
context.global.setSync('setTimeout', function (timeout) {
req.setTimeout(timeout);
});
context.evalSync(`
req = {
...req || {},
getUrl: global.getUrl,
setUrl: global.setUrl,
getMethod: global.getMethod,
getAuthMode: global.getAuthMode,
setMethod: global.setMethod,
getHeaders: global.getHeaders,
setHeaders: global.setHeaders,
getHeader: global.getHeader,
setHeader: global.setHeader,
getBody: global.getBody,
setBody: global.setBody,
setMaxRedirects: global.setMaxRedirects,
getTimeout: global.getTimeout,
setTimeout: global.setTimeout
}
`);
};
module.exports = addBrunoRequestShimToContext;

View File

@@ -0,0 +1,46 @@
const ivm = require('isolated-vm');
const addBrunoResponseShimToContext = (context, res) => {
context.global.setSync('status', new ivm.ExternalCopy(res?.status).copyInto());
context.global.setSync('headers', new ivm.ExternalCopy(res?.headers).copyInto());
context.global.setSync('body', new ivm.ExternalCopy(res?.body).copyInto());
context.global.setSync('responseTime', new ivm.ExternalCopy(res?.responseTime).copyInto());
context.global.setSync('getStatus', function () {
return res?.getStatus();
});
context.global.setSync('getHeader', function (name) {
return res?.getHeader(name);
});
context.global.setSync('getHeaders', function () {
return res?.getHeaders();
});
context.global.setSync('getBody', function () {
return res?.getBody();
});
context.global.setSync('getResponseTime', function () {
return res?.getResponseTime();
});
context.evalSync(`
res = {
...res || {},
status: global.status,
statusText: global.statusText,
headers: global.headers,
body: global.body,
responseTime: global.responseTime,
getStatus: global.getStatus,
getHeader: global.getHeader,
getHeaders: global.getHeaders,
getBody: global.getBody,
getResponseTime: global.getResponseTime
}
`);
};
module.exports = addBrunoResponseShimToContext;

View File

@@ -0,0 +1,39 @@
const addConsoleShimToContext = (context, console) => {
context.global.setSync('log', function (...args) {
console?.log && console.log(...args);
return args;
});
context.global.setSync('debug', function (...args) {
console?.debug && console.debug(...args);
return args;
});
context.global.setSync('info', function (...args) {
console?.info && console.info(...args);
return args;
});
context.global.setSync('warn', function (...args) {
console?.warn && console.warn(...args);
return args;
});
context.global.setSync('error', function (...args) {
console?.error && console.error(...args);
return args;
});
context.evalSync(`
console = {
...console || {},
log: global.log,
debug: global.debug,
info: global.info,
warn: global.warn,
error: global.error
}
`);
};
module.exports = addConsoleShimToContext;

View File

@@ -0,0 +1,57 @@
const axios = require('axios');
const ivm = require('isolated-vm');
const { cleanJson } = require('../../../../utils');
const addAxiosShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.axios = (...args) => $0.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
${['get', 'post', 'put', 'patch', 'delete']
?.map(
(method, idx) =>
`globalThis.axios.${method} = (...args) => $${
idx + 1
}.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)))`
)
.join('\n')}
globalThis.requireObject = {
...globalThis.requireObject,
axios: globalThis.axios,
}
`,
[
async (...argStrings) => {
console.log(argStrings);
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios(...args)
.then((response) => {
return cleanJson(response?.data);
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
...['get', 'post', 'put', 'patch', 'delete']?.map((method) => async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios[method](...args)
.then((response) => {
return cleanJson(response?.data);
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
})
],
{ arguments: { reference: true } }
);
};
module.exports = addAxiosShimToContext;

View File

@@ -0,0 +1,121 @@
const axios = require('axios');
const ivm = require('isolated-vm');
const { cleanJson } = require('../../../../utils');
const addAxiosShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.axios = (...args) => $0.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.get = (...args) => $1.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.post = (...args) => $2.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.put = (...args) => $3.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.delete = (...args) => $4.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.axios.patch = (...args) => $5.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.requireObject = {
...globalThis.requireObject,
axios: globalThis.axios,
}
`,
[
async (...argStrings) => {
console.log(argStrings);
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.get(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.post(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.put(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.delete(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
},
async (...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
const res = await axios
.patch(...args)
.then((response) => {
const { status, headers, data } = response || {};
return cleanJson({ status, headers, data });
})
.catch((err) => {
return {
message: err.message
// response: cleanJson(err.response)
};
});
return new ivm.ExternalCopy(res).copyInto({ release: true });
}
],
{ arguments: { reference: true } }
);
};
module.exports = addAxiosShimToContext;

View File

@@ -0,0 +1,11 @@
const addAxiosShimToContext = require('./axios');
const addNanoidShimToContext = require('./nanoid');
const addUuidShimToContext = require('./uuid');
const addLibraryShimsToContext = async (context) => {
await addAxiosShimToContext(context);
await addNanoidShimToContext(context);
await addUuidShimToContext(context);
};
module.exports = addLibraryShimsToContext;

View File

@@ -0,0 +1,23 @@
const ivm = require('isolated-vm');
const { nanoid } = require('nanoid');
const addNanoidShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.nanoid = {};
globalThis.nanoid.nanoid = () => $0.applySync(undefined);
globalThis.requireObject = {
...globalThis.requireObject,
nanoid: globalThis.nanoid
}
`,
[
() => {
return new ivm.ExternalCopy(nanoid()).copyInto({ release: true });
}
],
{ arguments: { reference: true } }
);
};
module.exports = addNanoidShimToContext;

View File

@@ -0,0 +1,39 @@
const ivm = require('isolated-vm');
const uuid = require('uuid');
const { MAX, NIL } = uuid;
const addUuidShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.uuid = {};
globalThis.uuid.MAX = $0;
globalThis.uuid.NIL = $1;
${['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate']
?.map(
(fn, idx) =>
`globalThis.uuid.${fn} = (...args) => $${
idx + 2
}.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));`
)
.join('\n')}
globalThis.requireObject = {
...globalThis.requireObject,
uuid: globalThis.uuid,
}
`,
[
new ivm.ExternalCopy(MAX).copyInto({ release: true }),
new ivm.ExternalCopy(NIL).copyInto({ release: true }),
...['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate']?.map(
(fn) =>
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(uuid[fn](...args)).copyInto({ release: true });
}
)
],
{ arguments: { reference: true } }
);
};
module.exports = addUuidShimToContext;

View File

@@ -0,0 +1,83 @@
const ivm = require('isolated-vm');
const { MAX, NIL, parse, stringify, v1, v1ToV6, v3, v4, v5, v6, v6ToV1, v7, validate, version } = require('uuid');
const addUuidShimToContext = async (context) => {
await context.evalClosure(
`
globalThis.uuid = {};
globalThis.uuid.MAX = $0;
globalThis.uuid.NIL = $1;
globalThis.uuid.version = (...args) => $2.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.parse = (...args) => $3.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.stringify = (...args) => $4.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v1 = (...args) => $5.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v1ToV6 = (...args) => $6.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v3 = (...args) => $7.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v4 = (...args) => $8.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v5 = (...args) => $9.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v6 = (...args) => $10.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v6ToV1 = (...args) => $11.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.v7 = (...args) => $12.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.uuid.validate = (...args) => $13.applySync(undefined, args?.map(arg=>JSON.stringify(arg)));
globalThis.requireObject = {
...globalThis.requireObject,
uuid: globalThis.uuid,
}
`,
[
new ivm.ExternalCopy(MAX).copyInto({ release: true }),
new ivm.ExternalCopy(NIL).copyInto({ release: true }),
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(version(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(parse(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(stringify(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v1(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v1ToV6(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v3(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v4(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v5(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v6(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v6ToV1(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(v7(...args)).copyInto({ release: true });
},
(...argStrings) => {
let args = argStrings?.map((arg) => JSON.parse(arg));
return new ivm.ExternalCopy(validate(...args)).copyInto({ release: true });
}
],
{ arguments: { reference: true } }
);
};
module.exports = addUuidShimToContext;

View File

@@ -0,0 +1,32 @@
const ivm = require('isolated-vm');
const addSleepShimToContext = (context, console) => {
context.evalClosureSync(
`
global.sleep = (...args) => $0.applySyncPromise(undefined, args?.map(arg=>JSON.stringify(arg)));
`,
[
async (...argStrings) => {
await new Promise((resolve) => {
const timer = Number(argStrings?.[0]);
if (!Number.isInteger(timer) || timer < 0) {
resolve();
}
setTimeout(() => {
resolve();
}, timer);
});
return new ivm.ExternalCopy('done').copyInto({ release: true });
}
],
{ arguments: { reference: true } }
);
context.evalSync(`
global.setTimeout = async (fn, timer) => {
await sleep(timer);
await fn.apply();
}
`);
};
module.exports = addSleepShimToContext;

View File

@@ -0,0 +1,57 @@
const addTestShimToContext = async (context, __brunoTestResults) => {
context.global.setSync('addResult', function (v) {
__brunoTestResults.addResult(v);
});
context.global.setSync('getResults', function () {
return __brunoTestResults.getResults();
});
context.evalSync(`
global.expect = require('chai').expect;
global.assert = require('chai').assert;
global.__brunoTestResults = {
addResult: global.addResult,
getResults: global.getResults,
}
global.DummyChaiAssertionError = class DummyChaiAssertionError extends Error {
constructor(message, props, ssf) {
super(message);
this.name = "AssertionError";
Object.assign(this, props);
}
}
global.Test = (__brunoTestResults) => async (description, callback) => {
try {
await callback();
__brunoTestResults.addResult({ description, status: "pass" });
} catch (error) {
if (error instanceof DummyChaiAssertionError) {
const { message, actual, expected } = error;
__brunoTestResults.addResult({
description,
status: "fail",
error: message,
actual,
expected,
});
} else {
__brunoTestResults.addResult({
description,
status: "fail",
error: error.message || "An unexpected error occurred.",
});
}
console.log(error);
}
};
global.test = Test(__brunoTestResults);
`);
};
module.exports = addTestShimToContext;

View File

@@ -0,0 +1,203 @@
const ivm = require('isolated-vm');
const addBruShimToContext = require('./shims/bru');
const addBrunoRequestShimToContext = require('./shims/bruno-request');
const addBrunoResponseShimToContext = require('./shims/bruno-response');
const addTestShimToContext = require('./shims/test');
const { newQuickJSAsyncWASMModule } = require('quickjs-emscripten');
// execute `npm run build:isolated-vm:inbuilt-modules` if the below file doesn't exist
const getBundledCode = require('../bundle-browser-rollup');
const toNumber = (value) => {
const num = Number(value);
return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);
};
const executeQuickJsVm = ({ script: externalScript, context: externalContext }) => {
if (!isNaN(Number(externalScript))) {
return Number(externalScript);
}
let result;
const isolate = new ivm.Isolate();
try {
const context = isolate.createContextSync();
context.global.setSync('global', context.global.derefInto());
const { bru, req, res } = externalContext;
context.evalSync(`
let bru = {};
let req = {};
let res = {};
`);
bru && addBruShimToContext(context, bru);
req && addBrunoRequestShimToContext(context, req);
res && addBrunoResponseShimToContext(context, res);
context.global.setSync('setResult', function (arg) {
result = arg;
});
const templateLiteralText = `
let value = \`${externalScript}\`;
setResult(value);
`;
const jsExpressionText = `
let value = ${externalScript};
setResult(value);
`;
let scriptText = scriptType === 'template-literal' ? templateLiteralText : jsExpressionText;
const script = isolate.compileScriptSync(scriptText);
script.runSync(context);
return result;
} catch (error) {
console.error('Error executing the script!', error);
}
isolate.dispose();
};
const executeQuickJsVmAsync = async ({
script: externalScript,
context: externalContext
}) => {
if (!isNaN(Number(externalScript))) {
return toNumber(externalScript);
}
try {
const module = await newQuickJSAsyncWASMModule()
const runtime = module.newRuntime()
const vm = runtime.newContext()
const bundledCode = getBundledCode?.toString() || '';
let bundledScript = `(${bundledCode})()`;
bundledScript += `
globalThis.require = (module) => {
return globalThis.requireObject[module];
}
`;
bundledScript += `
let bru = {
cwd: __bruno__cwd,
getEnvName: __bruno__getEnvName,
getProcessEnv: __bruno__getProcessEnv,
getEnvVar: __bruno__getEnvVar,
setEnvVar: __bruno__setEnvVar,
getVar: __bruno__getVar,
setVar: __bruno__setVar,
setNextRequest: __bruno__setNextRequest,
visualize: __bruno__visualize,
getSecretVar: __bruno__getSecretVar
};
let req = {
url: __bruno__req__url,
method: __bruno__req__method,
headers: __bruno__req__headers,
body: __bruno__req__body,
timeout: __bruno__req__timeout,
getUrl: __bruno__req__getUrl,
setUrl: __bruno__req__setUrl,
getMethod: __bruno__req__getMethod,
setMethod: __bruno__req__setMethod,
getAuthMode: __bruno__req__getAuthMode,
getHeaders: __bruno__req__getHeaders,
setHeaders: __bruno__req__setHeaders,
getHeader: __bruno__req__getHeader,
setHeader: __bruno__req__setHeader,
getBody: __bruno__req__getBody,
setBody: __bruno__req__setBody,
setMaxRedirects: __bruno__req__setMaxRedirects,
getTimeout: __bruno__req__getTimeout,
setTimeout: __bruno__req__setTimeout
};
let res = {
status: globalThis.__bruno__res__status,
headers: globalThis.__bruno__res__headers,
body: globalThis.__bruno__res__body,
responseTime: globalThis.__bruno__res__responseTime,
getStatus: globalThis.__bruno__res__getStatus,
getHeader: globalThis.__bruno__res__getHeader,
getHeaders: globalThis.__bruno__res__getHeaders,
getBody: globalThis.__bruno__res__getBody,
getResponseTime: globalThis.__bruno__res__getResponseTime
};
`;
const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext;
bru && addBruShimToContext(vm, bru);
req && addBrunoRequestShimToContext(vm, req);
res && addBrunoResponseShimToContext(vm, res);
test && __brunoTestResults && addTestShimToContext(vm, __brunoTestResults);
bundledScript += `
globalThis.expect = require('chai').expect;
globalThis.assert = require('chai').assert;
globalThis.__brunoTestResults = {
addResult: globalThis.__bruno__addResult,
getResults: globalThis.__bruno__getResults,
}
globalThis.DummyChaiAssertionError = class DummyChaiAssertionError extends Error {
constructor(message, props, ssf) {
super(message);
this.name = "AssertionError";
Object.assign(this, props);
}
}
globalThis.Test = (__brunoTestResults) => (description, callback) => {
try {
callback();
__brunoTestResults.addResult({ description, status: "pass" });
} catch (error) {
if (error instanceof DummyChaiAssertionError) {
const { message, actual, expected } = error;
__brunoTestResults.addResult({
description,
status: "fail",
error: message,
actual,
expected,
});
} else {
__brunoTestResults.addResult({
description,
status: "fail",
error: error.message || "An unexpected error occurred.",
});
}
// console.log(error);
}
};
globalThis.test = Test(__brunoTestResults);
`;
bundledScript += externalScript;
const result = await vm.evalCodeAsync(bundledScript);
if (result.error) {
console.log("Execution failed:", vm.dump(result.error))
result.error.dispose()
} else {
result.value.dispose();
}
vm.dispose();
return result;
} catch (error) {
console.error('Error executing the script!', error);
}
};
module.exports = {
executeQuickJsVm,
executeQuickJsVmAsync
};

View File

@@ -0,0 +1,65 @@
const { marshallToVm } = require('../utils');
const addBruShimToContext = (vm, bru) => {
let cwd = vm.newFunction('cwd', function () {
return marshallToVm(bru.cwd(), vm);
});
vm.setProp(vm.global, "__bruno__cwd", cwd)
cwd.dispose();
let getEnvName = vm.newFunction('getEnvName', function () {
return marshallToVm(bru.getEnvName(), vm);
});
vm.setProp(vm.global, "__bruno__getEnvName", getEnvName);
getEnvName.dispose();
let getProcessEnv = vm.newFunction('getProcessEnv', function (key) {
return marshallToVm(bru.getProcessEnv(vm.dump(key)), vm);
});
vm.setProp(vm.global, "__bruno__getProcessEnv", getProcessEnv);
getProcessEnv.dispose();
let getEnvVar = vm.newFunction('getEnvVar', function (key) {
return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);
});
vm.setProp(vm.global, "__bruno__getEnvVar", getEnvVar);
getEnvVar.dispose();
let setEnvVar = vm.newFunction('setEnvVar', function (key, value) {
bru.setEnvVar(vm.dump(key), vm.dump(value));
});
vm.setProp(vm.global, "__bruno__setEnvVar", setEnvVar);
setEnvVar.dispose();
let getVar = vm.newFunction('getVar', function (key) {
return marshallToVm(bru.getVar(vm.dump(key)), vm);
});
vm.setProp(vm.global, "__bruno__getVar", getVar);
getVar.dispose();
let setVar = vm.newFunction('setVar', function (key, value) {
bru.setVar(vm.dump(key), vm.dump(value));
});
vm.setProp(vm.global, "__bruno__setVar", setVar);
setVar.dispose();
let setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {
bru.setNextRequest(vm.dump(nextRequest));
});
vm.setProp(vm.global, "__bruno__setNextRequest", setNextRequest);
setNextRequest.dispose();
let visualize = vm.newFunction('visualize', function (htmlString) {
bru.visualize(vm.dump(htmlString));
});
vm.setProp(vm.global, "__bruno__visualize", visualize);
visualize.dispose();
let getSecretVar = vm.newFunction('getSecretVar', function (key) {
return marshallToVm(bru.getSecretVar(vm.dump(key)), vm);
});
vm.setProp(vm.global, "__bruno__getSecretVar", getSecretVar);
getSecretVar.dispose();
};
module.exports = addBruShimToContext;

View File

@@ -0,0 +1,107 @@
const { marshallToVm } = require('../utils');
const addBrunoRequestShimToContext = (vm, req) => {
const url = marshallToVm(req.getUrl(), vm);
const method = marshallToVm(req.getMethod(), vm);
const headers = marshallToVm(req.getHeaders(), vm);
const body = marshallToVm(req.getBody(), vm);
const timeout = marshallToVm(req.getTimeout(), vm);
vm.setProp(vm.global, '__bruno__req__url', url);
vm.setProp(vm.global, '__bruno__req__method', method);
vm.setProp(vm.global, '__bruno__req__headers', headers);
vm.setProp(vm.global, '__bruno__req__body', body);
vm.setProp(vm.global, '__bruno__req__timeout', timeout);
url.dispose();
method.dispose();
headers.dispose();
body.dispose();
timeout.dispose();
let getUrl = vm.newFunction('getUrl', function () {
return marshallToVm(req.getUrl(), vm);
});
vm.setProp(vm.global, '__bruno__req__getUrl', getUrl);
getUrl.dispose();
let setUrl = vm.newFunction('setUrl', function (url) {
req.setUrl(vm.dump(url));
});
vm.setProp(vm.global, '__bruno__req__setUrl', setUrl);
setUrl.dispose();
let getMethod = vm.newFunction('getMethod', function () {
return marshallToVm(req.getMethod(), vm);
});
vm.setProp(vm.global, '__bruno__req__getMethod', getMethod);
getMethod.dispose();
let getAuthMode = vm.newFunction('getAuthMode', function () {
return marshallToVm(req.getAuthMode(), vm);
});
vm.setProp(vm.global, '__bruno__req__getAuthMode', getAuthMode);
getAuthMode.dispose();
let setMethod = vm.newFunction('setMethod', function (method) {
req.setMethod(vm.dump(method));
});
vm.setProp(vm.global, '__bruno__req__setMethod', setMethod);
setMethod.dispose();
let getHeaders = vm.newFunction('getHeaders', function () {
return marshallToVm(req.getHeaders(), vm);
});
vm.setProp(vm.global, '__bruno__req__getHeaders', getHeaders);
getHeaders.dispose();
let setHeaders = vm.newFunction('setHeaders', function (headers) {
req.setHeaders(vm.dump(headers));
});
vm.setProp(vm.global, '__bruno__req__setHeaders', setHeaders);
setHeaders.dispose();
let getHeader = vm.newFunction('getHeader', function (name) {
return marshallToVm(req.getHeader(vm.dump(name)), vm);
});
vm.setProp(vm.global, '__bruno__req__getHeader', getHeader);
getHeader.dispose();
let setHeader = vm.newFunction('setHeader', function (name, value) {
req.setHeader(vm.dump(name), vm.dump(value));
});
vm.setProp(vm.global, '__bruno__req__setHeader', setHeader);
setHeader.dispose();
let getBody = vm.newFunction('getBody', function () {
return marshallToVm(req.getBody(), vm);
});
vm.setProp(vm.global, '__bruno__req__getBody', getBody);
getBody.dispose();
let setBody = vm.newFunction('setBody', function (data) {
req.setBody(vm.dump(data));
});
vm.setProp(vm.global, '__bruno__req__setBody', setBody);
setBody.dispose();
let setMaxRedirects = vm.newFunction('setMaxRedirects', function (maxRedirects) {
req.setMaxRedirects(vm.dump(maxRedirects));
});
vm.setProp(vm.global, '__bruno__req__setMaxRedirects', setMaxRedirects);
setMaxRedirects.dispose();
let getTimeout = vm.newFunction('getTimeout', function () {
return marshallToVm(req.getTimeout(), vm);
});
vm.setProp(vm.global, '__bruno__req__getTimeout', getTimeout);
getTimeout.dispose();
let setTimeout = vm.newFunction('setTimeout', function (timeout) {
req.setTimeout(vm.dump(timeout));
});
vm.setProp(vm.global, '__bruno__req__setTimeout', setTimeout);
setTimeout.dispose();
};
module.exports = addBrunoRequestShimToContext;

View File

@@ -0,0 +1,51 @@
const { marshallToVm } = require('../utils');
const addBrunoResponseShimToContext = (vm, res) => {
const status = marshallToVm(res?.status, vm);
const headers = marshallToVm(res?.headers, vm);
const body = marshallToVm(res?.body, vm);
const responseTime = marshallToVm(res?.responseTime, vm);
vm.setProp(vm.global, '__bruno__res__status', status);
vm.setProp(vm.global, '__bruno__res__headers', headers);
vm.setProp(vm.global, '__bruno__res__body', body);
vm.setProp(vm.global, '__bruno__res__responseTime', responseTime);
status.dispose();
headers.dispose();
body.dispose();
responseTime.dispose();
let getStatus = vm.newFunction('getStatus', function () {
return marshallToVm(res.getStatus(), vm);
});
vm.setProp(vm.global, '__bruno__res__getStatus', getStatus);
getStatus.dispose();
let getHeader = vm.newFunction('getHeader', function (name) {
return marshallToVm(res.getHeader(vm.dump(name)), vm);
});
vm.setProp(vm.global, '__bruno__res__getHeader', getHeader);
getHeader.dispose();
let getHeaders = vm.newFunction('getHeaders', function () {
return marshallToVm(res.getHeaders(), vm);
});
vm.setProp(vm.global, '__bruno__res__getHeaders', getHeaders);
getHeaders.dispose();
let getBody = vm.newFunction('getBody', function () {
return marshallToVm(res.getBody(), vm);
});
vm.setProp(vm.global, '__bruno__res__getBody', getBody);
getBody.dispose();
let getResponseTime = vm.newFunction('getResponseTime', function () {
return marshallToVm(res.getResponseTime(), vm);
});
vm.setProp(vm.global, '__bruno__res__getResponseTime', getResponseTime);
getResponseTime.dispose();
};
module.exports = addBrunoResponseShimToContext;

View File

@@ -0,0 +1,17 @@
const { marshallToVm } = require('../utils');
const addBruShimToContext = (vm, __brunoTestResults) => {
let addResult = vm.newFunction('addResult', function (v) {
__brunoTestResults.addResult(vm.dump(v));
});
vm.setProp(vm.global, "__bruno__addResult", addResult);
addResult.dispose();
let getResults = vm.newFunction('getResults', function () {
return marshallToVm(__brunoTestResults.getResults(), vm);
});
vm.setProp(vm.global, "__bruno__getResults", getResults);
getResults.dispose();
};
module.exports = addBruShimToContext;

View File

@@ -0,0 +1,35 @@
const marshallToVm = (value, vm) => {
if (value === undefined) {
return vm.undefined;
}
if (value === null) {
return vm.null;
}
if (typeof value === "string") {
return vm.newString(value);
} else if (typeof value === "number") {
return vm.newNumber(value);
} else if (typeof value === "boolean") {
return vm.newBoolean(value);
} else if (typeof value === "object") {
if (Array.isArray(value)) {
const arr = vm.newArray();
for (let i = 0; i < value.length; i++) {
vm.setProp(arr, i, marshallToVm(value[i], vm));
}
return arr;
} else {
const obj = vm.newObject();
for (const key in value) {
vm.setProp(obj, key, marshallToVm(value[key], vm));
}
return obj;
}
}
};
module.exports = {
marshallToVm
};

View File

@@ -35,7 +35,7 @@ describe('runtime', () => {
})
`;
const runtime = new TestRuntime();
const runtime = new TestRuntime({ runtime: 'vm2' });
const result = await runtime.runTests(
testFile,
{ ...baseRequest },
@@ -71,7 +71,7 @@ describe('runtime', () => {
})
`;
const runtime = new TestRuntime();
const runtime = new TestRuntime({ runtime: 'vm2' });
const result = await runtime.runTests(
testFile,
{ ...baseRequest },
@@ -114,7 +114,7 @@ describe('runtime', () => {
bru.setVar('validation', validate(new Date().toISOString()))
`;
const runtime = new ScriptRuntime();
const runtime = new ScriptRuntime({ runtime: 'vm2' });
const result = await runtime.runRequestScript(script, { ...baseRequest }, {}, {}, '.', null, process.env);
expect(result.runtimeVariables.validation).toBeTruthy();
});
@@ -160,7 +160,7 @@ describe('runtime', () => {
bru.setVar('validation', validate(new Date().toISOString()))
`;
const runtime = new ScriptRuntime();
const runtime = new ScriptRuntime({ runtime: 'vm2' });
const result = await runtime.runResponseScript(
script,
{ ...baseRequest },

View File

@@ -1,7 +1,7 @@
meta {
name: echo json
type: http
seq: 1
seq: 2
}
post {

View File

@@ -1,7 +1,7 @@
meta {
name: echo plaintext
type: http
seq: 2
seq: 3
}
post {

View File

@@ -1,7 +1,7 @@
meta {
name: echo xml parsed
type: http
seq: 3
seq: 4
}
post {

View File

@@ -1,7 +1,7 @@
meta {
name: echo xml raw
type: http
seq: 4
seq: 5
}
post {

View File

@@ -5,4 +5,6 @@ vars {
env.var1: envVar1
env-var2: envVar2
bark: {{process.env.PROC_ENV_VAR}}
foo: bar
testSetEnvVar: bruno-29653
}

View File

@@ -9,52 +9,3 @@ get {
body: none
auth: none
}
auth:awsv4 {
accessKeyId: a
secretAccessKey: b
sessionToken: c
service: d
region: e
profileName: f
}
vars:pre-request {
m4: true
pong: pong
}
assert {
res.status: eq 200
res.responseTime: lte 2000
res.body: eq {{pong}}
}
tests {
test("should ping pong", function() {
const data = res.getBody();
expect(data).to.equal(bru.getRequestVar("pong"));
});
}
docs {
# API Documentation
## Introduction
Welcome to the API documentation for [Your API Name]. This document provides instructions on how to make requests to the API and covers available authentication methods.
## Authentication
Before making requests to the API, you need to authenticate your application. [Your API Name] supports the following authentication methods:
### API Key
To use API key authentication, include your API key in the request headers as follows:
```http
GET /api/endpoint
Host: api.example.com
Authorization: Bearer YOUR_API_KEY
}

View File

@@ -0,0 +1,23 @@
meta {
name: getEnvName
type: http
seq: 1
}
get {
url: {{host}}/ping
body: none
auth: none
}
script:pre-request {
const envName = bru.getEnvName();
bru.setVar("testEnvName", envName);
}
tests {
test("should get env name in scripts", function() {
const testEnvName = bru.getVar("testEnvName");
expect(testEnvName).to.equal("Prod");
});
}

View File

@@ -0,0 +1,19 @@
meta {
name: getEnvVar
type: http
seq: 2
}
get {
url: {{host}}/ping
body: none
auth: none
}
tests {
test("should get env var in scripts", function() {
const host = bru.getEnvVar("host")
expect(host).to.equal("https://testbench-sanity.usebruno.com");
});
}

View File

@@ -0,0 +1,19 @@
meta {
name: getProcessEnv
type: http
seq: 6
}
get {
url: {{host}}/ping
body: none
auth: none
}
tests {
test("bru.getProcessEnv()", function() {
const v = bru.getProcessEnv("PROC_ENV_VAR");
expect(v).to.equal("woof");
});
}

View File

@@ -0,0 +1,19 @@
meta {
name: getVar
type: http
seq: 5
}
get {
url: {{host}}/ping
body: none
auth: none
}
tests {
test("should get var in scripts", function() {
const testSetVar = bru.getVar("testSetVar");
expect(testSetVar).to.equal("bruno-test-87267");
});
}

View File

@@ -0,0 +1,23 @@
meta {
name: setEnvVar
type: http
seq: 3
}
get {
url: {{host}}/ping
body: none
auth: none
}
script:post-response {
bru.setEnvVar("testSetEnvVar", "bruno-29653")
}
tests {
test("should set env var in scripts", function() {
const testSetEnvVar = bru.getEnvVar("testSetEnvVar")
expect(testSetEnvVar).to.equal("bruno-29653");
});
}

View File

@@ -0,0 +1,22 @@
meta {
name: setVar
type: http
seq: 4
}
get {
url: {{host}}/ping
body: none
auth: none
}
script:post-response {
bru.setVar("testSetVar", "bruno-test-87267")
}
tests {
test("should get var in scripts", function() {
const testSetVar = bru.getVar("testSetVar");
expect(testSetVar).to.equal("bruno-test-87267");
});
}

View File

@@ -0,0 +1,40 @@
meta {
name: getBody
type: http
seq: 9
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": "bruno"
}
}
assert {
res.status: eq 200
}
tests {
test("req.getBody()", function() {
const data = res.getBody();
expect(data).to.eql({
"hello": "bruno"
});
});
}

View File

@@ -0,0 +1,28 @@
meta {
name: getHeader
type: http
seq: 5
}
get {
url: {{host}}/ping
body: none
auth: none
}
headers {
bruno: is-awesome
}
assert {
res.status: eq 200
res.body: eq pong
}
tests {
test("req.getHeader(name)", function() {
const h = req.getHeader('bruno');
expect(h).to.equal("is-awesome");
});
}

View File

@@ -0,0 +1,30 @@
meta {
name: getHeaders
type: http
seq: 7
}
get {
url: {{host}}/ping
body: none
auth: none
}
headers {
bruno: is-awesome
della: is-beautiful
}
assert {
res.status: eq 200
res.body: eq pong
}
tests {
test("req.getHeaders()", function() {
const h = req.getHeaders();
expect(h.bruno).to.equal("is-awesome");
expect(h.della).to.equal("is-beautiful");
});
}

View File

@@ -0,0 +1,24 @@
meta {
name: getMethod
type: http
seq: 3
}
get {
url: {{host}}/ping
body: none
auth: none
}
assert {
res.status: eq 200
res.body: eq pong
}
tests {
test("req.getMethod()()", function() {
const method = req.getMethod();
expect(method).to.equal("GET");
});
}

View File

@@ -0,0 +1,23 @@
meta {
name: getUrl
type: http
seq: 1
}
get {
url: {{host}}/ping
body: none
auth: none
}
assert {
res.status: eq 200
res.body: eq pong
}
tests {
test("req.getUrl()", function() {
const url = req.getUrl();
expect(url).to.equal("https://testbench-sanity.usebruno.com/ping");
});
}

View File

@@ -0,0 +1,46 @@
meta {
name: setBody
type: http
seq: 10
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": "bruno"
}
}
assert {
res.status: eq 200
}
script:pre-request {
req.setBody({
"bruno": "is awesome"
});
}
tests {
test("req.setBody()", function() {
const data = res.getBody();
expect(data).to.eql({
"bruno": "is awesome"
});
});
}

View File

@@ -0,0 +1,32 @@
meta {
name: setHeader
type: http
seq: 6
}
get {
url: {{host}}/ping
body: none
auth: none
}
headers {
bruno: is-awesome
}
assert {
res.status: eq 200
res.body: eq pong
}
script:pre-request {
req.setHeader('bruno', 'is-the-future');
}
tests {
test("req.setHeader(name)", function() {
const h = req.getHeader('bruno');
expect(h).to.equal("is-the-future");
});
}

View File

@@ -0,0 +1,37 @@
meta {
name: setHeaders
type: http
seq: 8
}
get {
url: {{host}}/ping
body: none
auth: none
}
headers {
bruno: is-awesome
della: is-beautiful
}
assert {
res.status: eq 200
res.body: eq pong
}
script:pre-request {
req.setHeaders({
"content-type": "application/text",
"transaction-id": "foobar"
});
}
tests {
test("req.setHeaders()", function() {
const h = req.getHeaders();
expect(h['content-type']).to.equal("application/text");
expect(h['transaction-id']).to.equal("foobar");
});
}

View File

@@ -0,0 +1,28 @@
meta {
name: setMethod
type: http
seq: 4
}
post {
url: {{host}}/ping
body: none
auth: none
}
assert {
res.status: eq 200
res.body: eq pong
}
script:pre-request {
req.setMethod("GET");
}
tests {
test("req.setMethod()()", function() {
const method = req.getMethod();
expect(method).to.equal("GET");
});
}

View File

@@ -0,0 +1,28 @@
meta {
name: setUrl
type: http
seq: 2
}
get {
url: {{host}}/ping/invalid
body: none
auth: none
}
assert {
res.status: eq 200
res.body: eq pong
}
script:pre-request {
req.setUrl("https://testbench-sanity.usebruno.com/ping");
}
tests {
test("req.setUrl()", function() {
const url = req.getUrl();
expect(url).to.equal("https://testbench-sanity.usebruno.com/ping");
});
}

View File

@@ -0,0 +1,40 @@
meta {
name: getBody
type: http
seq: 4
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": "bruno"
}
}
assert {
res.status: eq 200
}
tests {
test("res.getBody()", function() {
const data = res.getBody();
expect(data).to.eql({
"hello": "bruno"
});
});
}

View File

@@ -0,0 +1,38 @@
meta {
name: getHeader
type: http
seq: 2
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": "bruno"
}
}
assert {
res.status: eq 200
}
tests {
test("res.getHeader(name)", function() {
const server = res.getHeader('x-powered-by');
expect(server).to.eql('Express');
});
}

View File

@@ -0,0 +1,39 @@
meta {
name: getHeaders
type: http
seq: 3
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": "bruno"
}
}
assert {
res.status: eq 200
}
tests {
test("res.getHeaders(name)", function() {
const h = res.getHeaders();
expect(h['x-powered-by']).to.eql('Express');
expect(h['content-length']).to.eql('17');
});
}

View File

@@ -0,0 +1,39 @@
meta {
name: getResponseTime
type: http
seq: 5
}
post {
url: {{host}}/api/echo/json
body: json
auth: none
}
auth:basic {
username: asd
password: j
}
auth:bearer {
token:
}
body:json {
{
"hello": "bruno"
}
}
assert {
res.status: eq 200
}
tests {
test("res.getResponseTime()", function() {
const responseTime = res.getResponseTime();
expect(typeof responseTime).to.eql("number");
expect(responseTime > 0).to.be.true;
});
}

View File

@@ -0,0 +1,24 @@
meta {
name: getStatus
type: http
seq: 1
}
get {
url: {{host}}/ping
body: none
auth: none
}
assert {
res.status: eq 200
res.body: eq pong
}
tests {
test("res.getStatus()", function() {
const status = res.getStatus()
expect(status).to.equal(200);
});
}

View File

@@ -1,54 +0,0 @@
meta {
name: get-env-name
type: http
seq: 1
}
get {
url: {{host}}/ping
body: none
auth: none
}
auth:awsv4 {
accessKeyId: a
secretAccessKey: b
sessionToken: c
service: d
region: e
profileName: f
}
script:pre-request {
const envName = bru.getEnvName();
bru.setVar("testEnvName", envName);
}
tests {
test("should get env name in scripts", function() {
const testEnvName = bru.getVar("testEnvName");
expect(testEnvName).to.equal("Prod");
});
}
docs {
# API Documentation
## Introduction
Welcome to the API documentation for [Your API Name]. This document provides instructions on how to make requests to the API and covers available authentication methods.
## Authentication
Before making requests to the API, you need to authenticate your application. [Your API Name] supports the following authentication methods:
### API Key
To use API key authentication, include your API key in the request headers as follows:
```http
GET /api/endpoint
Host: api.example.com
Authorization: Bearer YOUR_API_KEY
}

View File

@@ -1,49 +0,0 @@
meta {
name: get-env-var
type: http
seq: 2
}
get {
url: {{host}}/ping
body: none
auth: none
}
auth:awsv4 {
accessKeyId: a
secretAccessKey: b
sessionToken: c
service: d
region: e
profileName: f
}
tests {
test("should get env var in scripts", function() {
const host = bru.getEnvVar("host")
expect(host).to.equal("https://testbench-sanity.usebruno.com");
});
}
docs {
# API Documentation
## Introduction
Welcome to the API documentation for [Your API Name]. This document provides instructions on how to make requests to the API and covers available authentication methods.
## Authentication
Before making requests to the API, you need to authenticate your application. [Your API Name] supports the following authentication methods:
### API Key
To use API key authentication, include your API key in the request headers as follows:
```http
GET /api/endpoint
Host: api.example.com
Authorization: Bearer YOUR_API_KEY
}

View File

@@ -0,0 +1,34 @@
meta {
name: axios-pre-req-script
type: http
seq: 1
}
get {
url: {{host}}/ping
body: none
auth: none
}
script:pre-request {
const axios = require("axios");
const url = "https://testbench-sanity.usebruno.com/api/echo/json";
const response = await axios.post(url, {
"hello": "bruno"
});
req.setBody(response.data);
req.setMethod("POST");
req.setUrl(url);
}
tests {
test("req.getBody()", function() {
const data = res.getBody();
expect(data).to.eql({
"hello": "bruno"
});
});
}

View File

@@ -0,0 +1,32 @@
meta {
name: setTimeout
type: http
seq: 1
}
get {
url: {{host}}/ping
body: none
auth: none
}
script:pre-request {
bru.setVar("test-js-set-timeout", "");
await new Promise((resolve, reject) => {
setTimeout(() => {
bru.setVar("test-js-set-timeout", "bruno");
resolve();
}, 1000);
});
const v = bru.getVar("test-js-set-timeout");
bru.setVar("test-js-set-timeout", v + "-is-awesome");
}
tests {
test("setTimeout()", function() {
const v = bru.getVar("test-js-set-timeout")
expect(v).to.eql("bruno-is-awesome");
});
}

View File

@@ -1,54 +0,0 @@
meta {
name: set-env-var
type: http
seq: 3
}
get {
url: {{host}}/ping
body: none
auth: none
}
auth:awsv4 {
accessKeyId: a
secretAccessKey: b
sessionToken: c
service: d
region: e
profileName: f
}
script:post-response {
bru.setEnvVar("testSetEnvVar", "bruno-29653")
}
tests {
test("should set env var in scripts", function() {
const testSetEnvVar = bru.getEnvVar("testSetEnvVar")
console.log(testSetEnvVar);
expect(testSetEnvVar).to.equal("bruno-29653");
});
}
docs {
# API Documentation
## Introduction
Welcome to the API documentation for [Your API Name]. This document provides instructions on how to make requests to the API and covers available authentication methods.
## Authentication
Before making requests to the API, you need to authenticate your application. [Your API Name] supports the following authentication methods:
### API Key
To use API key authentication, include your API key in the request headers as follows:
```http
GET /api/endpoint
Host: api.example.com
Authorization: Bearer YOUR_API_KEY
}