Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4c03c89
chore(playground): add script library dependencies
sundram-bruno Aug 23, 2026
9af53bb
feat(playground): support desktop safe-mode script libraries in the q…
sundram-bruno Aug 23, 2026
bd1e0f3
feat(playground): lazy-load the request runner on first send
sundram-bruno Aug 23, 2026
209d909
fix(playground): keep script-set headers over the auth config
sundram-bruno Aug 23, 2026
47dcec3
test(playground): add script library coverage
sundram-bruno Aug 23, 2026
7ddd5c0
chore(playground): restore uuid type declarations for the build
sundram-bruno Aug 23, 2026
88a2411
fix(playground): harden the script sandbox per review
sundram-bruno Aug 26, 2026
632ce24
test(playground): extend sandbox coverage and simplify the script e2e
sundram-bruno Aug 26, 2026
288610e
Merge remote-tracking branch 'upstream/main' into feat/playground-scr…
sundram-bruno Aug 26, 2026
3ce2ad9
fix(playground): match jsonwebtoken behaviour for string payloads and…
sundram-bruno Aug 26, 2026
a1d2ec7
test(playground): type the jwt sign result before string ops
sundram-bruno Aug 26, 2026
7a1cb6d
chore(playground): drop redundant comment in the typed-array helper
sundram-bruno Aug 27, 2026
9e476d6
Merge remote-tracking branch 'upstream/main' into feat/playground-scr…
sundram-bruno Aug 27, 2026
08a2398
feat(playground): scope jsonwebtoken out of the sandbox
sundram-bruno Aug 28, 2026
b6c44dc
fix(playground): address code review feedback on the sandbox
sundram-bruno Aug 28, 2026
dec1f57
build(playground): rebuild the library bundle before the standalone b…
sundram-bruno Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 178 additions & 32 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { test, expect } from '../../playwright';
import type { Page } from '@playwright/test';
import type { CodeEditorComponent } from '../../components/code-editor/code-editor.component';

const LIBRARY_TESTS_SCRIPT = `
const moment = require('moment');
const CryptoJS = require('crypto-js');
const { v4, validate } = require('uuid');
const { nanoid } = require('nanoid');
const tv4 = require('tv4');

test('moment formats a date', function () {
expect(moment('2026-01-02').format('YYYY-MM-DD')).to.equal('2026-01-02');
});

test('crypto-js hashes and uuid validates', function () {
expect(CryptoJS.SHA256('abc').toString()).to.equal('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
expect(validate(v4())).to.equal(true);
expect(nanoid(10)).to.have.lengthOf(10);
});

test('tv4 validates against a schema', function () {
expect(tv4.validate({ a: 1 }, { type: 'object' })).to.equal(true);
});
`;

const setEditorScript = async (page: Page, editor: CodeEditorComponent, script: string): Promise<void> => {
await editor.focus();
await page.keyboard.press('ControlOrMeta+a');
await page.keyboard.insertText(script);
};

test.describe('playground script execution', () => {
test.use({ viewport: { width: 1280, height: 900 } });

test('runs a tests script using the safe-mode libraries on Send', async ({ page, playground, responsePane }) => {
await page.route('**/api/users**', (route) =>
route.fulfill({
status: 200,
headers: { 'content-type': 'application/json', 'access-control-allow-origin': '*' },
body: JSON.stringify({ users: [{ id: 1, name: 'Ada' }] })
})
);

await page.goto('/#/?pg=1&dock=bottom');
await playground.openSidebarItem('get users');

await playground.selectTab('tests');
await setEditorScript(page, playground.testsEditor, LIBRARY_TESTS_SCRIPT);

await responsePane.send();
await responsePane.switchToTab('tests');

await expect(page.getByText(/Passed: [1-9]\d*, Failed: 0/).first()).toBeVisible();
await expect(page.getByText(/Failed: [1-9]/)).toHaveCount(0);
});
});
16 changes: 15 additions & 1 deletion packages/bruno-api-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"build:lib-bundle": "vite build --config vite.config.libraries-bundle.ts",
"prebuild": "npm run gen:theme && npm run build:lib-bundle",
"build": "npm run clean && tsc -p tsconfig.build.json --emitDeclarationOnly && vite build",
"prebuild:standalone": "npm run gen:theme && npm run build:lib-bundle",
"build:standalone": "npm run clean:standalone && vite build --config vite.config.standalone.ts",
"build:all": "npm run build && npm run build:standalone",
"prepublishOnly": "npm run build:all",
Expand Down Expand Up @@ -73,9 +74,13 @@
"@types/markdown-it": "^14.1.2",
"@types/prismjs": "^1.26.5",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"buffer": "^6.0.3",
"chai": "~5.3.3",
"codemirror": "^6.0.2",
"crypto-js": "^4.2.0",
"fast-json-format": "~0.4.0",
"fuse.js": "^7.5.0",
"js-md5": "^0.9.2",
Expand All @@ -84,12 +89,14 @@
"jsonpath-plus": "^10.3.0",
"lodash-es": "~4.17.21",
"markdown-it": "^14.1.0",
"moment": "^2.30.1",
"monaco-editor": "^0.53.0",
"nanoid": "~3.3.11",
"node-html-parser": "^8.0.4",
"path-browserify": "^1.0.1",
"prettier": "^2.7.1",
"prismjs": "^1.29.0",
"quickjs-emscripten": "~0.31.0",
"quickjs-emscripten": "~0.32.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-markdown": "^10.0.0",
Expand All @@ -99,6 +106,8 @@
"react-router-dom": "^7.3.0",
"remark-gfm": "^4.0.1",
"strip-json-comments": "^3.1.1",
"tv4": "^1.3.0",
"uuid": "^10.0.0",
"xml-formatter": "^3.5.0"
},
"devDependencies": {
Expand All @@ -108,12 +117,17 @@
"@playwright/test": "^1.52.0",
"@tailwindcss/postcss": "^4.1.13",
"@tailwindcss/typography": "^0.5.10",
"@types/atob": "^2.1.4",
"@types/btoa": "^1.2.5",
"@types/crypto-js": "^4.2.2",
"@types/express": "^4.17.21",
"@types/lodash-es": "~4.17.12",
"@types/node": "^26.2.0",
"@types/path-browserify": "^1.0.3",
"@types/prismjs": "^1.26.3",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/tv4": "^1.2.33",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { HttpRequest, HttpRequestHeader } from '@opencollection/types/reque
import type { OpenCollection as OpenCollectionCollection } from '@opencollection/types';
import type { Item } from '@opencollection/types/collection/item';
import type { Auth } from '@opencollection/types/common/auth';
import { requestRunner } from '@/runner';
import { getAncestorsByUuid } from '@/utils/fileUtils';
import { ItemVariableResolverProvider } from '@/hooks';
import TitleLabel from '@/components/TitleLabel/TitleLabel';
Expand Down Expand Up @@ -41,7 +40,6 @@ const HttpRequestPlaygroundView: React.FC<PlaygroundViewProps> = ({ item, collec
// The request/response split is one draggable divider whose axis follows the
// orientation: horizontal layout resizes width, vertical layout resizes height.
const { size: paneSize, isResizing, containerRef, startResize } = useSplitPane(orientation);
const runner = useMemo(() => requestRunner, []);
const ancestry = useMemo(
() => (collection && itemUuid ? getAncestorsByUuid(collection, itemUuid) : []),
[collection, itemUuid]
Expand Down Expand Up @@ -114,7 +112,8 @@ const HttpRequestPlaygroundView: React.FC<PlaygroundViewProps> = ({ item, collec
const environment = envs.find(
(env: any) => env.name === selectedEnvironment
);
const result = await runner.runRequest({
const { requestRunner } = await import('@/runner');
const result = await requestRunner.runRequest({
item: editableItem,
collection,
environment,
Expand All @@ -139,7 +138,7 @@ const HttpRequestPlaygroundView: React.FC<PlaygroundViewProps> = ({ item, collec
} finally {
setIsLoading(false);
}
}, [collection, editableItem, runner, selectedEnvironment, itemUuid]);
}, [collection, editableItem, selectedEnvironment, itemUuid, dispatch]);

return (
<ItemVariableResolverProvider
Expand Down
65 changes: 65 additions & 0 deletions packages/bruno-api-docs/src/runner/RequestExecutor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,71 @@
expect(fetchMock.mock.calls[0][0]).toBe('https://api.example.com/data?api_key=secret123');
});

describe('auth header precedence', () => {
const jsonResponse = {
status: 200,
statusText: 'OK',
url: 'https://api.example.com/data',
headers: new Headers({ 'content-type': 'application/json' }),
text: async () => JSON.stringify({ ok: true })
};

it('keeps an existing Authorization header over the bearer auth config', async () => {
const fetchMock = vi.fn().mockResolvedValue(jsonResponse);
global.fetch = fetchMock as unknown as typeof fetch;

await new RequestExecutor().executeRequest({
name: 'script-set auth wins',
type: 'http',
http: {
method: 'GET',
url: 'https://api.example.com/data',
headers: [{ name: 'authorization', value: 'Bearer script-signed-token' }],
auth: { type: 'bearer', token: 'config-token' }
}
} as any);

Check warning on line 88 in packages/bruno-api-docs/src/runner/RequestExecutor.spec.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type

expect(fetchMock.mock.calls[0][1].headers['authorization']).toBe('Bearer script-signed-token');
expect(fetchMock.mock.calls[0][1].headers['Authorization']).toBeUndefined();
});

it('applies the bearer auth config when no Authorization header exists', async () => {
const fetchMock = vi.fn().mockResolvedValue(jsonResponse);
global.fetch = fetchMock as unknown as typeof fetch;

await new RequestExecutor().executeRequest({
name: 'auth config applies',
type: 'http',
http: {
method: 'GET',
url: 'https://api.example.com/data',
auth: { type: 'bearer', token: 'config-token' }
}
} as any);

Check warning on line 106 in packages/bruno-api-docs/src/runner/RequestExecutor.spec.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type

expect(fetchMock.mock.calls[0][1].headers['Authorization']).toBe('Bearer config-token');
});

it('keeps an existing header over an apikey header placement', async () => {
const fetchMock = vi.fn().mockResolvedValue(jsonResponse);
global.fetch = fetchMock as unknown as typeof fetch;

await new RequestExecutor().executeRequest({
name: 'apikey header respects existing',
type: 'http',
http: {
method: 'GET',
url: 'https://api.example.com/data',
headers: [{ name: 'X-Api-Key', value: 'from-script' }],
auth: { type: 'apikey', key: 'x-api-key', value: 'from-config', placement: 'header' }
}
} as any);

Check warning on line 124 in packages/bruno-api-docs/src/runner/RequestExecutor.spec.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type

expect(fetchMock.mock.calls[0][1].headers['X-Api-Key']).toBe('from-script');
expect(fetchMock.mock.calls[0][1].headers['x-api-key']).toBeUndefined();
});
});

describe('request body', () => {
const sendWithBody = async (method: string) => {
const fetchMock = vi.fn().mockResolvedValue({
Expand Down
9 changes: 6 additions & 3 deletions packages/bruno-api-docs/src/runner/RequestExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,21 +191,24 @@ export class RequestExecutor {
}

private setAuthHeaders(headers: Record<string, string>, auth: any) {
const hasHeader = (name: string) =>
Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase());

switch (auth.type) {
case 'basic':
if (auth.username && auth.password) {
if (auth.username && auth.password && !hasHeader('Authorization')) {
const credentials = btoa(`${auth.username}:${auth.password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
break;
case 'bearer':
if (auth.token) {
if (auth.token && !hasHeader('Authorization')) {
headers['Authorization'] = `Bearer ${auth.token}`;
}
break;
case 'apikey':
if (auth.key && auth.value) {
if (auth.placement === 'header') {
if (auth.placement === 'header' && !hasHeader(auth.key)) {
headers[auth.key] = auth.value;
}
}
Expand Down
4 changes: 3 additions & 1 deletion packages/bruno-api-docs/src/sampleCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ request:
bru.setVar('collection-var-set-by-collection-script', 'collection-var-value-set-by-collection-script');
}
- type: after-response
code: wefewfewfewfewfwefwefewfewfewfewfewfewfewfewf
code: |-
// Collection · post-response (L0)
console.log('POST > L0 collection');
- type: tests
code: |-
// used by \`scripting/js/folder-collection script-tests\`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,41 @@
import { expect, assert } from 'chai';
// todo: add all the supported libraries
import { Buffer } from 'buffer';
import moment from 'moment';
import btoa from 'btoa';
import atob from 'atob/node-atob.js';
import CryptoJS from 'crypto-js';
import tv4 from 'tv4';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import * as uuid from 'uuid';
import * as nanoid from 'nanoid';
import path from 'path-browserify';

(globalThis as any).expect = expect;

Check warning on line 14 in packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type
(globalThis as any).assert = assert;

Check warning on line 15 in packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type
(globalThis as any).moment = moment;

Check warning on line 16 in packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type
(globalThis as any).btoa = btoa;

Check warning on line 17 in packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type
(globalThis as any).atob = atob;

Check warning on line 18 in packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type
(globalThis as any).Buffer = Buffer;

Check warning on line 19 in packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type
(globalThis as any).tv4 = tv4;

Check warning on line 20 in packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts

View workflow job for this annotation

GitHub Actions / lint_unit_tests_and_builds

Unexpected any. Specify a different type
(globalThis as any).Ajv = Ajv;
(globalThis as any).addFormats = addFormats;
(globalThis as any).uuid = uuid;
(globalThis as any).nanoid = nanoid;
(globalThis as any).path = path;

(globalThis as any).requireObject = {
...((globalThis as any).requireObject || {}),
chai: { expect, assert }
'chai': { expect, assert },
'moment': moment,
'buffer': { Buffer },
'btoa': btoa,
'atob': atob,
'crypto-js': CryptoJS,
'tv4': tv4,
'ajv': Ajv,
'ajv-formats': addFormats,
'uuid': uuid,
'nanoid': nanoid,
'path': path
};
Loading
Loading