From 4c03c89b784bd4937c890626e944d3cfeb4be3fc Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Sun, 23 Aug 2026 21:45:33 +0530 Subject: [PATCH 01/11] chore(playground): add script library dependencies --- package-lock.json | 236 ++++++++++++++---- packages/bruno-api-docs/package.json | 16 +- .../vite.config.libraries-bundle.ts | 3 +- 3 files changed, 203 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1972bda1..86cbc6a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1612,45 +1612,45 @@ } }, "node_modules/@jitl/quickjs-ffi-types": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.31.0.tgz", - "integrity": "sha512-1yrgvXlmXH2oNj3eFTrkwacGJbmM0crwipA3ohCrjv52gBeDaD7PsTvFYinlAnqU8iPME3LGP437yk05a2oejw==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.32.0.tgz", + "integrity": "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==", "license": "MIT" }, "node_modules/@jitl/quickjs-wasmfile-debug-asyncify": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.31.0.tgz", - "integrity": "sha512-YkdzQdr1uaftFhgEnTRjTTZHk2SFZdpWO7XhOmRVbi6CEVsH9g5oNF8Ta1q3OuSJHRwwT8YsuR1YzEiEIJEk6w==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.32.0.tgz", + "integrity": "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw==", "license": "MIT", "dependencies": { - "@jitl/quickjs-ffi-types": "0.31.0" + "@jitl/quickjs-ffi-types": "0.32.0" } }, "node_modules/@jitl/quickjs-wasmfile-debug-sync": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.31.0.tgz", - "integrity": "sha512-8XvloaaWBONqcHXYs5tWOjdhQVxzULilIfB2hvZfS6S+fI4m2+lFiwQy7xeP8ExHmiZ7D8gZGChNkdLgjGfknw==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.32.0.tgz", + "integrity": "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q==", "license": "MIT", "dependencies": { - "@jitl/quickjs-ffi-types": "0.31.0" + "@jitl/quickjs-ffi-types": "0.32.0" } }, "node_modules/@jitl/quickjs-wasmfile-release-asyncify": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.31.0.tgz", - "integrity": "sha512-uz0BbQYTxNsFkvkurd7vk2dOg57ElTBLCuvNtRl4rgrtbC++NIndD5qv2+AXb6yXDD3Uy1O2PCwmoaH0eXgEOg==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.32.0.tgz", + "integrity": "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q==", "license": "MIT", "dependencies": { - "@jitl/quickjs-ffi-types": "0.31.0" + "@jitl/quickjs-ffi-types": "0.32.0" } }, "node_modules/@jitl/quickjs-wasmfile-release-sync": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.31.0.tgz", - "integrity": "sha512-hYduecOByj9AsAfsJhZh5nA6exokmuFC8cls39+lYmTCGY51bgjJJJwReEu7Ff7vBWaQCL6TeDdVlnp2WYz0jw==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.32.0.tgz", + "integrity": "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg==", "license": "MIT", "dependencies": { - "@jitl/quickjs-ffi-types": "0.31.0" + "@jitl/quickjs-ffi-types": "0.32.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -1892,9 +1892,9 @@ "license": "MIT" }, "node_modules/@microlink/react-json-view": { - "version": "1.31.25", - "resolved": "https://registry.npmjs.org/@microlink/react-json-view/-/react-json-view-1.31.25.tgz", - "integrity": "sha512-eVRAzd6GpIWhUwZkk1BHVtyS3Sgcw9eVJYvV/RGxahmV10TD3Z50cZNg4EQOzlEWDw+FeitTtr/Aj1yp7fRYXA==", + "version": "1.31.28", + "resolved": "https://registry.npmjs.org/@microlink/react-json-view/-/react-json-view-1.31.28.tgz", + "integrity": "sha512-G4lQITsTrf+yZGrP6171dCb4dGusFrrSrNMaJ6TTISx2Cfp5qYDYb2eI3DepwDkdB+HzTq2OV52m1jzc+riNuw==", "license": "MIT", "dependencies": { "react-base16-styling": "~0.10.0", @@ -2783,6 +2783,13 @@ "react-dom": ">=16.8" } }, + "node_modules/@types/atob": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/atob/-/atob-2.1.4.tgz", + "integrity": "sha512-FisOhG87cCFqzCgq6FUtSYsTMOHCB/p28zJbSN1QBo4ZGJfg9PEhMjdIV++NDeOnloUUe0Gz6jwBV+L1Ac00Mw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2839,6 +2846,16 @@ "@types/node": "*" } }, + "node_modules/@types/btoa": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/btoa/-/btoa-1.2.5.tgz", + "integrity": "sha512-BItINdjZRlcGdI2efwK4bwxY5vEAT0SnIVfMOZVT18wp4900F1Lurqk/9PNdF9hMP1zgFmWbjVEtAsQKVcbqxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -2849,6 +2866,13 @@ "@types/node": "*" } }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -3011,6 +3035,13 @@ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, + "node_modules/@types/path-browserify": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/path-browserify/-/path-browserify-1.0.3.tgz", + "integrity": "sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prismjs": { "version": "1.26.6", "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", @@ -3090,6 +3121,13 @@ "integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==", "license": "MIT" }, + "node_modules/@types/tv4": { + "version": "1.2.33", + "resolved": "https://registry.npmjs.org/@types/tv4/-/tv4-1.2.33.tgz", + "integrity": "sha512-7phCVTXC6Bj50IV1iKOwqGkR4JONJyMbRZnKTSuujv1S/tO9rG5OdCt7BMSjytO+zJmYdn1/I4fd3SH0gtO99g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3102,13 +3140,6 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", @@ -3807,6 +3838,18 @@ "node": ">= 0.4" } }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/autoprefixer": { "version": "10.5.4", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", @@ -4077,6 +4120,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "license": "(MIT OR Apache-2.0)", + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -4550,6 +4605,13 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "deprecated": "Active development of CryptoJS has been discontinued. This library is no longer maintained.", + "license": "MIT" + }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -6342,9 +6404,9 @@ } }, "node_modules/human-id": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.0.tgz", - "integrity": "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.1.tgz", + "integrity": "sha512-zPGsiS+dWoTZtZ4AtpA9Y+BdSFSNWvnouNlWNoUFyAM6xHOHmdCvqO3k8AIbdamCOv4gUFUVNPf6rJFfc4UiJw==", "dev": true, "license": "MIT", "bin": { @@ -8609,6 +8671,15 @@ "license": "MIT", "optional": true }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/monaco-editor": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.53.0.tgz", @@ -9117,6 +9188,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9511,28 +9588,28 @@ "license": "MIT" }, "node_modules/quickjs-emscripten": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/quickjs-emscripten/-/quickjs-emscripten-0.31.0.tgz", - "integrity": "sha512-K7Yt78aRPLjPcqv3fIuLW1jW3pvwO21B9pmFOolsjM/57ZhdVXBr51GqJpalgBlkPu9foAvhEAuuQPnvIGvLvQ==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/quickjs-emscripten/-/quickjs-emscripten-0.32.0.tgz", + "integrity": "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==", "license": "MIT", "dependencies": { - "@jitl/quickjs-wasmfile-debug-asyncify": "0.31.0", - "@jitl/quickjs-wasmfile-debug-sync": "0.31.0", - "@jitl/quickjs-wasmfile-release-asyncify": "0.31.0", - "@jitl/quickjs-wasmfile-release-sync": "0.31.0", - "quickjs-emscripten-core": "0.31.0" + "@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-debug-sync": "0.32.0", + "@jitl/quickjs-wasmfile-release-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-release-sync": "0.32.0", + "quickjs-emscripten-core": "0.32.0" }, "engines": { "node": ">=16.0.0" } }, "node_modules/quickjs-emscripten-core": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/quickjs-emscripten-core/-/quickjs-emscripten-core-0.31.0.tgz", - "integrity": "sha512-oQz8p0SiKDBc1TC7ZBK2fr0GoSHZKA0jZIeXxsnCyCs4y32FStzCW4d1h6E1sE0uHDMbGITbk2zhNaytaoJwXQ==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/quickjs-emscripten-core/-/quickjs-emscripten-core-0.32.0.tgz", + "integrity": "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg==", "license": "MIT", "dependencies": { - "@jitl/quickjs-ffi-types": "0.31.0" + "@jitl/quickjs-ffi-types": "0.32.0" } }, "node_modules/railroad-diagrams": { @@ -9822,9 +9899,9 @@ } }, "node_modules/read-yaml-file/node_modules/js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "license": "MIT", "dependencies": { @@ -11218,6 +11295,24 @@ "node": "*" } }, + "node_modules/tv4": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", + "integrity": "sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==", + "license": [ + { + "type": "Public Domain", + "url": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, + { + "type": "MIT", + "url": "http://jsonary.com/LICENSE.txt" + } + ], + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11568,6 +11663,20 @@ "devOptional": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -13138,9 +13247,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-yaml": "^4.1.0", @@ -13148,12 +13261,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", @@ -13163,6 +13278,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": { @@ -13172,12 +13289,16 @@ "@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/path-browserify": "^1.0.3", "@types/prismjs": "^1.26.3", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "@types/uuid": "^10.0.0", + "@types/tv4": "^1.2.33", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "concurrently": "^9.1.2", @@ -13194,6 +13315,23 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } + }, + "packages/bruno-api-docs/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } } } } diff --git a/packages/bruno-api-docs/package.json b/packages/bruno-api-docs/package.json index 44c625c0..c1c4b0e6 100644 --- a/packages/bruno-api-docs/package.json +++ b/packages/bruno-api-docs/package.json @@ -73,8 +73,16 @@ "@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", + "crypto-js": "^4.2.0", + "moment": "^2.30.1", + "path-browserify": "^1.0.1", + "tv4": "^1.3.0", + "uuid": "^10.0.0", "codemirror": "^6.0.2", "fast-json-format": "~0.4.0", "fuse.js": "^7.5.0", @@ -88,7 +96,7 @@ "node-html-parser": "^8.0.4", "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", @@ -112,7 +120,11 @@ "@types/prismjs": "^1.26.3", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "@types/uuid": "^10.0.0", + "@types/atob": "^2.1.4", + "@types/btoa": "^1.2.5", + "@types/crypto-js": "^4.2.2", + "@types/path-browserify": "^1.0.3", + "@types/tv4": "^1.2.33", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "concurrently": "^9.1.2", diff --git a/packages/bruno-api-docs/vite.config.libraries-bundle.ts b/packages/bruno-api-docs/vite.config.libraries-bundle.ts index f2fcbeb3..e5618b24 100644 --- a/packages/bruno-api-docs/vite.config.libraries-bundle.ts +++ b/packages/bruno-api-docs/vite.config.libraries-bundle.ts @@ -24,7 +24,8 @@ export default defineConfig({ resolve: { alias: { '@slices': resolve(__dirname, 'src/store/slices'), - '@': resolve(__dirname, 'src') + '@': resolve(__dirname, 'src'), + 'atob': 'atob/node-atob.js' } }, build: { From 9af53bb6360f906e391ab928a2a33bebc745c2e3 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Sun, 23 Aug 2026 21:45:36 +0530 Subject: [PATCH 02/11] feat(playground): support desktop safe-mode script libraries in the quickjs sandbox --- .../scripting/sandbox/quickjs/bundle-entry.ts | 42 ++- .../src/scripting/sandbox/quickjs/index.ts | 55 ++-- .../src/scripting/sandbox/quickjs/lib/jwt.ts | 282 ++++++++++++++++++ .../sandbox/quickjs/shims/lib/axios.ts | 181 +++++++++++ .../sandbox/quickjs/shims/lib/crypto-utils.ts | 86 ++++++ .../sandbox/quickjs/shims/lib/utils.ts | 44 +++ .../sandbox/quickjs/shims/local-module.ts | 25 ++ .../sandbox/quickjs/shims/require.ts | 52 ++++ 8 files changed, 728 insertions(+), 39 deletions(-) create mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts create mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/axios.ts create mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/crypto-utils.ts create mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts create mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/local-module.ts create mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/require.ts diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts index 00b696e9..637f8b26 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/bundle-entry.ts @@ -1,6 +1,44 @@ 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'; +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'; +import jwt from './lib/jwt'; + +(globalThis as any).expect = expect; +(globalThis as any).assert = assert; +(globalThis as any).moment = moment; +(globalThis as any).btoa = btoa; +(globalThis as any).atob = atob; +(globalThis as any).Buffer = Buffer; +(globalThis as any).tv4 = tv4; +(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).jwt = jwt; + (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, + 'jsonwebtoken': jwt }; diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/index.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/index.ts index f40604a8..8483cdfd 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/index.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/index.ts @@ -3,9 +3,13 @@ import addBrunoRequestShimToContext from './shims/bruno-request'; import addConsoleShimToContext from './shims/console'; import addBrunoResponseShimToContext from './shims/bruno-response'; import addTestShimToContext from './shims/test'; +import addCryptoUtilsShimToContext from './shims/lib/crypto-utils'; +import addAxiosShimToContext from './shims/lib/axios'; import { newQuickJSWASMModule, memoizePromiseFactory } from 'quickjs-emscripten'; import { marshallToVm } from './utils'; import { getBundledCode } from './bundled-libraries.iife.js'; +import addLocalModuleShimToContext from './shims/local-module'; +import { getRequireCode } from './shims/require'; let QuickJSSyncContext: any; const loader = memoizePromiseFactory(() => newQuickJSWASMModule()); @@ -95,51 +99,26 @@ const executeQuickJsVmAsync = async ({ externalScript = externalScript?.trim(); try { - const module = await newQuickJSWASMModule(); + const module = await loader(); const vm = module.newContext(); - const bundledCode = getBundledCode?.toString() || ''; + addCryptoUtilsShimToContext(vm); + addLocalModuleShimToContext(vm); - const moduleLoaderCode = function () { - return ` - globalThis.require = (mod) => { - let lib = globalThis.requireObject[mod]; - let isModuleAPath = (module) => (module?.startsWith('.') || module?.startsWith?.('')) - if (lib) { - return lib; - } - else if (isModuleAPath(mod)) { - // fetch local module - let localModuleCode = globalThis.__brunoLoadLocalModule(mod); - - // compile local module as iife - (function (){ - const initModuleExportsCode = "const module = { exports: {} };" - const copyModuleExportsCode = "\\n;globalThis.requireObject[mod] = module.exports;"; - const patchedRequire = ${` - "\\n;" + - "let require = (subModule) => isModuleAPath(subModule) ? globalThis.require(path.resolve('', mod, '..', subModule)) : globalThis.require(subModule)" + - "\\n;" - `} - eval(initModuleExportsCode + patchedRequire + localModuleCode + copyModuleExportsCode); - })(); - - // resolve module - return globalThis.requireObject[mod]; - } - else { - throw new Error("Cannot find module " + mod); - } - } - `; - }; + const bundledCode = getBundledCode?.toString() || ''; - vm.evalCode( + const bootResult = vm.evalCode( ` (${bundledCode})(); - ${moduleLoaderCode()} + ${getRequireCode()} ` ); + if (bootResult.error) { + const bootError = vm.dump(bootResult.error); + bootResult.error.dispose(); + throw new Error(`Failed to load sandbox libraries: ${bootError?.message || String(bootError)}`); + } + bootResult.value.dispose(); const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext; @@ -149,6 +128,8 @@ const executeQuickJsVmAsync = async ({ if (res) addBrunoResponseShimToContext(vm, res); if (test && __brunoTestResults) addTestShimToContext(vm, __brunoTestResults); + addAxiosShimToContext(vm); + const script = ` (async () => { const setTimeout = async(fn, timer) => { diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts new file mode 100644 index 00000000..fef21819 --- /dev/null +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts @@ -0,0 +1,282 @@ +import CryptoJS from 'crypto-js'; + +type HmacFn = (message: string, secret: CryptoJS.lib.WordArray | string) => CryptoJS.lib.WordArray; + +const SUPPORTED_ALGORITHMS: Record = { + HS256: (message, secret) => CryptoJS.HmacSHA256(message, secret), + HS384: (message, secret) => CryptoJS.HmacSHA384(message, secret), + HS512: (message, secret) => CryptoJS.HmacSHA512(message, secret) +}; + +const ASYMMETRIC_ALGORITHM_PATTERN = /^(RS|ES|PS)\d{3}$/; + +const unsupportedAlgorithmMessage = (algorithm: string) => + `${algorithm} is not supported in the docs playground; HS256, HS384 and HS512 are available. ` + + 'Asymmetric algorithms need a private key, which should never be embedded in a published collection script.'; + +const DURATION_UNIT_MS: Record = { + ms: 1, + s: 1000, + m: 60 * 1000, + h: 60 * 60 * 1000, + d: 24 * 60 * 60 * 1000, + w: 7 * 24 * 60 * 60 * 1000, + y: 365.25 * 24 * 60 * 60 * 1000 +}; + +const DURATION_PATTERN + = /^(\d+(?:\.\d+)?)\s*(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i; + +const normalizeDurationUnit = (rawUnit: string): string => { + const unit = rawUnit.toLowerCase(); + if (unit.startsWith('ms') || unit.startsWith('mil')) return 'ms'; + return unit[0]; +}; + +const parseDurationToSeconds = (value: number | string, optionName: string): number => { + if (typeof value === 'number') { + return value; + } + const match = DURATION_PATTERN.exec(String(value).trim()); + if (!match) { + throw new Error(`"${optionName}" should be a number of seconds or string representing a timespan`); + } + const amount = parseFloat(match[1]); + const unitKey = normalizeDurationUnit(match[2] || 'ms'); + return Math.floor((amount * DURATION_UNIT_MS[unitKey]) / 1000); +}; + +const toHmacSecret = (secret: any): CryptoJS.lib.WordArray | string => { + if (typeof secret === 'string') { + return secret; + } + if (secret && typeof secret === 'object' && typeof secret.length === 'number') { + const bytes = Array.from(secret as ArrayLike); + const words: number[] = []; + for (let i = 0; i < bytes.length; i++) { + words[i >>> 2] = (words[i >>> 2] || 0) | (bytes[i] << (24 - (i % 4) * 8)); + } + return CryptoJS.lib.WordArray.create(words, bytes.length); + } + return String(secret); +}; + +const base64UrlFromWordArray = (words: CryptoJS.lib.WordArray): string => { + return words.toString(CryptoJS.enc.Base64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +}; + +const base64UrlEncodeJson = (value: unknown): string => { + return base64UrlFromWordArray(CryptoJS.enc.Utf8.parse(JSON.stringify(value))); +}; + +const base64UrlDecodeToString = (segment: string): string => { + let base64 = segment.replace(/-/g, '+').replace(/_/g, '/'); + while (base64.length % 4 !== 0) { + base64 += '='; + } + return CryptoJS.enc.Base64.parse(base64).toString(CryptoJS.enc.Utf8); +}; + +const constantTimeEquals = (a: string, b: string): boolean => { + if (a.length !== b.length) { + return false; + } + let diff = 0; + for (let i = 0; i < a.length; i++) { + diff |= a.charCodeAt(i) ^ b.charCodeAt(i); + } + return diff === 0; +}; + +const nowInSeconds = () => Math.floor(Date.now() / 1000); + +const resolveCallback = (options: any, callback: any) => { + if (typeof options === 'function') { + return { resolvedOptions: undefined, resolvedCallback: options }; + } + return { resolvedOptions: options, resolvedCallback: callback }; +}; + +const withOptionalCallback = (callback: any, run: () => T): T | undefined => { + if (typeof callback !== 'function') { + return run(); + } + try { + callback(null, run()); + } catch (error) { + callback(error, undefined); + } + return undefined; +}; + +const matchesAudience = (audienceClaim: any, expected: any): boolean => { + const claims: any[] = Array.isArray(audienceClaim) ? audienceClaim : [audienceClaim]; + const expectations: any[] = Array.isArray(expected) ? expected : [expected]; + return expectations.some((expectation) => + claims.some((claim) => + expectation instanceof RegExp ? expectation.test(String(claim)) : expectation === claim + ) + ); +}; + +const signSync = (payload: any, secret: any, options: any): string => { + const algorithm = options?.algorithm || 'HS256'; + const hmac = SUPPORTED_ALGORITHMS[algorithm]; + if (!hmac) { + throw new Error( + ASYMMETRIC_ALGORITHM_PATTERN.test(algorithm) ? unsupportedAlgorithmMessage(algorithm) : 'invalid algorithm' + ); + } + if (secret === undefined || secret === null || secret === '') { + throw new Error('secretOrPrivateKey must have a value'); + } + + const isObjectPayload = typeof payload === 'object' && payload !== null; + const claims: any = isObjectPayload && options?.mutatePayload !== true ? { ...payload } : payload; + + if (isObjectPayload) { + if (options?.noTimestamp !== true && claims.iat === undefined) { + claims.iat = nowInSeconds(); + } + if (options?.expiresIn !== undefined) { + claims.exp = (claims.iat ?? nowInSeconds()) + parseDurationToSeconds(options.expiresIn, 'expiresIn'); + } + if (options?.notBefore !== undefined) { + claims.nbf = (claims.iat ?? nowInSeconds()) + parseDurationToSeconds(options.notBefore, 'notBefore'); + } + if (options?.audience !== undefined) claims.aud = options.audience; + if (options?.issuer !== undefined) claims.iss = options.issuer; + if (options?.subject !== undefined) claims.sub = options.subject; + if (options?.jwtid !== undefined) claims.jti = options.jwtid; + } + + const header = { alg: algorithm, typ: 'JWT', ...(options?.header || {}) }; + const signingInput = `${base64UrlEncodeJson(header)}.${base64UrlEncodeJson(claims)}`; + const signature = base64UrlFromWordArray(hmac(signingInput, toHmacSecret(secret))); + return `${signingInput}.${signature}`; +}; + +const decode = (token: any, options?: any): any => { + if (typeof token !== 'string') { + return null; + } + const segments = token.split('.'); + if (segments.length !== 3) { + return null; + } + try { + const header = JSON.parse(base64UrlDecodeToString(segments[0])); + const payloadText = base64UrlDecodeToString(segments[1]); + let payload: any; + try { + payload = JSON.parse(payloadText); + } catch { + if (options?.json) { + return null; + } + payload = payloadText; + } + if (options?.complete) { + return { header, payload, signature: segments[2] }; + } + return payload; + } catch { + return null; + } +}; + +const verifySync = (token: any, secret: any, options: any): any => { + if (!token) { + throw new Error('jwt must be provided'); + } + if (typeof token !== 'string' || token.split('.').length !== 3) { + throw new Error('jwt malformed'); + } + + const [headerSegment, payloadSegment, signatureSegment] = token.split('.'); + if (signatureSegment === '') { + throw new Error('jwt signature is required'); + } + + const decoded = decode(token, { complete: true }); + if (!decoded) { + throw new Error('jwt malformed'); + } + + const algorithm = decoded.header?.alg; + const hmac = SUPPORTED_ALGORITHMS[algorithm]; + if (!hmac) { + throw new Error( + ASYMMETRIC_ALGORITHM_PATTERN.test(String(algorithm)) ? unsupportedAlgorithmMessage(String(algorithm)) : 'invalid algorithm' + ); + } + if (options?.algorithms && !options.algorithms.includes(algorithm)) { + throw new Error('invalid algorithm'); + } + + const expectedSignature = base64UrlFromWordArray(hmac(`${headerSegment}.${payloadSegment}`, toHmacSecret(secret))); + if (!constantTimeEquals(expectedSignature, signatureSegment)) { + throw new Error('invalid signature'); + } + + const payload = decoded.payload; + const clockTimestamp = options?.clockTimestamp ?? nowInSeconds(); + const clockTolerance = options?.clockTolerance ?? 0; + + if (typeof payload === 'object' && payload !== null) { + const notBeforeApplies = payload.nbf !== undefined && options?.ignoreNotBefore !== true; + if (notBeforeApplies && payload.nbf > clockTimestamp + clockTolerance) { + throw new Error('jwt not active'); + } + const expiryApplies = payload.exp !== undefined && options?.ignoreExpiration !== true; + if (expiryApplies && payload.exp <= clockTimestamp - clockTolerance) { + throw new Error('jwt expired'); + } + if (options?.audience !== undefined) { + if (payload.aud === undefined || !matchesAudience(payload.aud, options.audience)) { + throw new Error(`jwt audience invalid. expected: ${[].concat(options.audience).join(' or ')}`); + } + } + if (options?.issuer !== undefined) { + const issuers = Array.isArray(options.issuer) ? options.issuer : [options.issuer]; + if (!issuers.includes(payload.iss)) { + throw new Error(`jwt issuer invalid. expected: ${issuers.join(',')}`); + } + } + if (options?.subject !== undefined && payload.sub !== options.subject) { + throw new Error(`jwt subject invalid. expected: ${options.subject}`); + } + if (options?.jwtid !== undefined && payload.jti !== options.jwtid) { + throw new Error(`jwt jwtid invalid. expected: ${options.jwtid}`); + } + if (options?.nonce !== undefined && payload.nonce !== options.nonce) { + throw new Error('jwt nonce invalid. expected: ' + options.nonce); + } + if (options?.maxAge !== undefined) { + if (payload.iat === undefined) { + throw new Error('iat required when maxAge is specified'); + } + const maxAgeSeconds = parseDurationToSeconds(options.maxAge, 'maxAge'); + if (payload.iat + maxAgeSeconds <= clockTimestamp - clockTolerance) { + throw new Error('maxAge exceeded'); + } + } + } + + if (options?.complete) { + return decoded; + } + return payload; +}; + +const sign = (payload: any, secret: any, options?: any, callback?: any) => { + const { resolvedOptions, resolvedCallback } = resolveCallback(options, callback); + return withOptionalCallback(resolvedCallback, () => signSync(payload, secret, resolvedOptions)); +}; + +const verify = (token: any, secret: any, options?: any, callback?: any) => { + const { resolvedOptions, resolvedCallback } = resolveCallback(options, callback); + return withOptionalCallback(resolvedCallback, () => verifySync(token, secret, resolvedOptions)); +}; + +export default { sign, verify, decode }; diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/axios.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/axios.ts new file mode 100644 index 00000000..06ddbd34 --- /dev/null +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/axios.ts @@ -0,0 +1,181 @@ +import { cleanJson } from '@/scripting/utils/common'; +import { marshallToVm } from '../../utils'; + +const METHODS = ['get', 'post', 'put', 'delete', 'patch']; +const METHODS_WITH_BODY = new Set(['post', 'put', 'patch']); + +interface AxiosShimConfig { + url?: string; + method?: string; + headers?: Record; + params?: Record; + data?: any; + timeout?: number; + auth?: { username?: string; password?: string }; +} + +const normalizeArgs = (method: string | null, args: any[]): AxiosShimConfig => { + if (method === null) { + if (typeof args[0] === 'string') { + return { ...(args[1] || {}), url: args[0], method: (args[1]?.method || 'get') }; + } + return { method: 'get', ...(args[0] || {}) }; + } + + if (METHODS_WITH_BODY.has(method)) { + return { ...(args[2] || {}), url: args[0], data: args[1], method }; + } + return { ...(args[1] || {}), url: args[0], method }; +}; + +const buildRequestUrl = (config: AxiosShimConfig): string => { + const url = config.url || ''; + if (!config.params || typeof config.params !== 'object') { + return url; + } + + const query = new URLSearchParams(); + Object.entries(config.params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + query.append(key, String(value)); + } + }); + const queryString = query.toString(); + if (!queryString) { + return url; + } + return url + (url.includes('?') ? '&' : '?') + queryString; +}; + +const buildAxiosError = (partial: Record, config: AxiosShimConfig) => { + return { + isAxiosError: true, + ...partial, + config: { + url: config.url, + method: config.method, + headers: config.headers, + data: config.data + } + }; +}; + +const parseResponseData = async (response: Response) => { + const contentType = response.headers.get('content-type') || ''; + const text = await response.text(); + if (contentType.includes('json') && text.length) { + try { + return JSON.parse(text); + } catch { + return text; + } + } + return text; +}; + +const performAxiosRequest = async (config: AxiosShimConfig) => { + if (typeof config.url !== 'string' || config.url.trim() === '') { + throw buildAxiosError({ message: `Invalid URL: ${String(config.url)}`, code: 'ERR_INVALID_URL' }, config); + } + + const headers: Record = { ...(config.headers || {}) }; + + if (config.auth?.username !== undefined) { + headers['Authorization'] = 'Basic ' + btoa(`${config.auth.username || ''}:${config.auth.password || ''}`); + } + + let body: string | undefined; + if (config.data !== undefined && config.data !== null) { + if (typeof config.data === 'object') { + body = JSON.stringify(config.data); + const hasContentType = Object.keys(headers).some((key) => key.toLowerCase() === 'content-type'); + if (!hasContentType) { + headers['Content-Type'] = 'application/json'; + } + } else { + body = String(config.data); + } + } + + const fetchOptions: RequestInit = { + method: (config.method || 'get').toUpperCase(), + headers, + ...(body !== undefined && { body }), + ...(config.timeout && { signal: AbortSignal.timeout(config.timeout) }) + }; + + let response: Response; + try { + response = await fetch(buildRequestUrl(config), fetchOptions); + } catch (err: any) { + if (err?.name === 'TimeoutError') { + throw buildAxiosError({ message: `timeout of ${config.timeout}ms exceeded`, code: 'ECONNABORTED' }, config); + } + throw buildAxiosError( + { + message: 'Network Error', + code: 'ERR_NETWORK', + hint: 'The browser blocked or failed the request. If the API works in this Bruno app, the server may not allow cross-origin (CORS) requests from this docs site.' + }, + config + ); + } + + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + const data = await parseResponseData(response); + + if (response.status < 200 || response.status >= 300) { + throw buildAxiosError( + { + message: `Request failed with status code ${response.status}`, + code: response.status >= 500 ? 'ERR_BAD_RESPONSE' : 'ERR_BAD_REQUEST', + response: { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + data + } + }, + config + ); + } + + return { status: response.status, headers: responseHeaders, data }; +}; + +const addAxiosShimToContext = (vm: any) => { + const registerAxiosFunction = (name: string, method: string | null) => { + const fnHandle = vm.newFunction(name, (...args: any[]) => { + const nativeArgs = args.map(vm.dump); + const config = normalizeArgs(method, nativeArgs); + const promise = vm.newPromise(); + performAxiosRequest(config) + .then((response) => { + promise.resolve(marshallToVm(cleanJson(response), vm)); + }) + .catch((err) => { + promise.reject(marshallToVm(cleanJson(err), vm)); + }); + promise.settled.then(vm.runtime.executePendingJobs); + return promise.handle; + }); + fnHandle.consume((handle: any) => vm.setProp(vm.global, name, handle)); + }; + + registerAxiosFunction('__bruno__axios', null); + METHODS.forEach((method) => registerAxiosFunction(`__bruno__axios__${method}`, method)); + + vm.evalCode(` + globalThis.axios = __bruno__axios; + ${METHODS.map((method) => `globalThis.axios.${method} = __bruno__axios__${method};`).join('\n')} + globalThis.requireObject = { + ...globalThis.requireObject, + axios: globalThis.axios, + }; + `); +}; + +export default addAxiosShimToContext; diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/crypto-utils.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/crypto-utils.ts new file mode 100644 index 00000000..70fab289 --- /dev/null +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/crypto-utils.ts @@ -0,0 +1,86 @@ +import { marshallToVm } from '../../utils'; +import { serializeTypedArray, deserializeTypedArray } from './utils'; + +const MAX_RANDOM_BYTES_PER_CALL = 65536; + +const addCryptoUtilsShimToContext = (vm: any) => { + const randomBytesHandle = vm.newFunction('randomBytes', (sizeHandle: any) => { + try { + let size = vm.dump(sizeHandle); + if (typeof size !== 'number') { + throw new TypeError('The "size" argument must be of type number'); + } + + size = Math.trunc(size); + + if (size < 0) { + throw new RangeError('The "size" argument must be >= 0'); + } + + if (size > MAX_RANDOM_BYTES_PER_CALL) { + throw new RangeError('The "size" argument is too large'); + } + + if (size === 0) { + return marshallToVm([], vm); + } + + const bytes = new Uint8Array(size); + globalThis.crypto.getRandomValues(bytes); + + return marshallToVm(Array.from(bytes), vm); + } catch (error: any) { + const vmError = vm.newError(error.message); + vm.setProp(vmError, 'name', vm.newString(error.name)); + throw vmError; + } + }); + + const getRandomValuesHandle = vm.newFunction('getRandomValues', (arrayHandle: any) => { + try { + const serializedArray = vm.dump(arrayHandle); + const typedArray = deserializeTypedArray(serializedArray); + + if (typedArray.length === 0) { + return marshallToVm([], vm); + } + + if (typedArray.length > MAX_RANDOM_BYTES_PER_CALL) { + throw new Error('getRandomValues: ArrayBufferView byte length exceeds 65536'); + } + + globalThis.crypto.getRandomValues(typedArray); + + return marshallToVm(Array.from(typedArray), vm); + } catch (error: any) { + const vmError = vm.newError(error.message); + vm.setProp(vmError, 'name', vm.newString(error.name)); + throw vmError; + } + }); + + vm.setProp(vm.global, '__bruno__crypto__randomBytes', randomBytesHandle); + vm.setProp(vm.global, '__bruno__crypto__getRandomValues', getRandomValuesHandle); + randomBytesHandle.dispose(); + getRandomValuesHandle.dispose(); + + vm.evalCode(` + ${serializeTypedArray.toString()} + + const cryptoModule = { + randomBytes: function(size) { + const byteArray = globalThis.__bruno__crypto__randomBytes(size); + return Buffer.from(Array.from(byteArray)); + }, + getRandomValues: function(typedArray) { + const serializedTypedArray = serializeTypedArray(typedArray); + typedArray.set(globalThis.__bruno__crypto__getRandomValues(serializedTypedArray)); + return typedArray; + }, + }; + + globalThis.crypto = cryptoModule; + `); +}; + +export default addCryptoUtilsShimToContext; diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts new file mode 100644 index 00000000..f8d1ed48 --- /dev/null +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts @@ -0,0 +1,44 @@ +const ALLOWED_TYPED_ARRAYS = new Set([ + 'Int8Array', + 'Uint8Array', + 'Uint8ClampedArray', + 'Int16Array', + 'Uint16Array', + 'Int32Array', + 'Uint32Array', + 'Float32Array', + 'Float64Array', + 'BigInt64Array', + 'BigUint64Array' +]); + +function serializeTypedArray(ta: any) { + return { + type: ta.constructor.name, + array: Array.from(ta), + length: ta.length + }; +} + +function deserializeTypedArray(obj: any) { + if (!obj || typeof obj !== 'object') { + throw new TypeError('getRandomValues: Invalid typed array object'); + } + + if (typeof obj.type !== 'string' || !ALLOWED_TYPED_ARRAYS.has(obj.type)) { + throw new TypeError(`getRandomValues: Invalid or unsupported typed array type: ${obj.type}`); + } + + if (!obj.array || typeof obj.length !== 'number') { + throw new TypeError('getRandomValues: Invalid typed array properties'); + } + + const TypedArrayConstructor = (globalThis as any)[obj.type]; + if (typeof TypedArrayConstructor !== 'function') { + throw new TypeError(`getRandomValues: Constructor ${obj.type} is not available`); + } + + return new TypedArrayConstructor(obj.array, 0, obj.length); +} + +export { serializeTypedArray, deserializeTypedArray }; diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/local-module.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/local-module.ts new file mode 100644 index 00000000..f6df759d --- /dev/null +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/local-module.ts @@ -0,0 +1,25 @@ +import { marshallToVm } from '../utils'; + +type LocalModuleResolver = (modulePath: string) => string | null; + +let localModuleResolver: LocalModuleResolver = () => null; + +export const setLocalModuleResolver = (resolver: LocalModuleResolver) => { + localModuleResolver = resolver; +}; + +const addLocalModuleShimToContext = (vm: any) => { + const loadLocalModule = vm.newFunction('__brunoLoadLocalModule', (pathHandle: any) => { + const modulePath = String(vm.dump(pathHandle)); + const source = localModuleResolver(modulePath); + if (source === null || source === undefined) { + throw vm.newError( + `Local file require is not available in the docs playground (tried to load '${modulePath}'). ` + 'Only the built-in safe-mode libraries can be required here.' + ); + } + return marshallToVm(source, vm); + }); + loadLocalModule.consume((handle: any) => vm.setProp(vm.global, '__brunoLoadLocalModule', handle)); +}; + +export default addLocalModuleShimToContext; diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/require.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/require.ts new file mode 100644 index 00000000..0d5c58be --- /dev/null +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/require.ts @@ -0,0 +1,52 @@ +const NODE_BUILTIN_MODULES = [ + 'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns', 'events', + 'fs', 'http', 'http2', 'https', 'net', 'os', 'perf_hooks', 'process', 'querystring', + 'readline', 'stream', 'string_decoder', 'timers', 'tls', 'url', 'util', 'v8', 'vm', + 'worker_threads', 'zlib' +]; + +const DEVELOPER_MODE_LIBRARIES = [ + 'lodash', 'cheerio', 'xml2js', 'node-fetch', 'yaml', 'json-query', + 'xml-formatter', 'chai-string', 'handlebars' +]; + +export const getRequireCode = () => ` + globalThis.require = (mod) => { + const lib = globalThis.requireObject[mod]; + if (lib) { + return lib; + } + + const collectionCwd = bru.cwd(); + const isModuleAPath = (module) => (module?.startsWith('.') || (!!collectionCwd && module?.startsWith?.(collectionCwd))); + if (isModuleAPath(mod)) { + const localModuleCode = globalThis.__brunoLoadLocalModule(mod); + (function () { + const initModuleExportsCode = "const module = { exports: {} };"; + const copyModuleExportsCode = "\\n;globalThis.requireObject[mod] = module.exports;"; + const patchedRequire = + "\\n;" + + "let require = (subModule) => isModuleAPath(subModule) ? globalThis.require(path.resolve(bru.cwd(), mod, '..', subModule)) : globalThis.require(subModule)" + + "\\n;"; + eval(initModuleExportsCode + patchedRequire + localModuleCode + copyModuleExportsCode); + })(); + return globalThis.requireObject[mod]; + } + + const bareName = mod?.startsWith?.('node:') ? mod.slice(5) : mod; + if (${JSON.stringify(NODE_BUILTIN_MODULES)}.includes(bareName)) { + throw new Error( + "'" + mod + "' is a Node.js builtin and is not available in the docs playground; " + + "it requires the Bruno desktop app's developer mode." + ); + } + if (${JSON.stringify(DEVELOPER_MODE_LIBRARIES)}.includes(bareName)) { + throw new Error( + "'" + mod + "' is only available in the Bruno desktop app's developer mode; " + + "the docs playground supports the safe-mode library set (chai, moment, uuid, nanoid, axios, " + + "crypto-js, jsonwebtoken, tv4, ajv, ajv-formats, path, buffer, btoa, atob)." + ); + } + throw new Error("Cannot find module " + mod); + } +`; From bd1e0f3f46eb58469a80c4316e8d69bddc2ca6b4 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Sun, 23 Aug 2026 21:45:37 +0530 Subject: [PATCH 03/11] feat(playground): lazy-load the request runner on first send --- .../Content/Views/PlaygroundView/PlaygroundView.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/PlaygroundView.tsx b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/PlaygroundView.tsx index c2d1bbce..3e03fa9e 100644 --- a/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/PlaygroundView.tsx +++ b/packages/bruno-api-docs/src/components/Playground/Content/Views/PlaygroundView/PlaygroundView.tsx @@ -2,7 +2,6 @@ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import type { HttpRequest } from '@opencollection/types/requests/http'; import type { OpenCollection as OpenCollectionCollection } from '@opencollection/types'; import type { Item } from '@opencollection/types/collection/item'; -import { requestRunner } from '@/runner'; import { getAncestorsByUuid } from '@/utils/fileUtils'; import { ItemVariableResolverProvider } from '@/hooks'; import TitleLabel from '@/components/TitleLabel/TitleLabel'; @@ -35,7 +34,6 @@ const HttpRequestPlaygroundView: React.FC = ({ 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] @@ -87,7 +85,8 @@ const HttpRequestPlaygroundView: React.FC = ({ 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, @@ -105,7 +104,7 @@ const HttpRequestPlaygroundView: React.FC = ({ item, collec } finally { setIsLoading(false); } - }, [collection, editableItem, runner, selectedEnvironment, itemUuid]); + }, [collection, editableItem, selectedEnvironment, itemUuid]); return ( Date: Sun, 23 Aug 2026 21:45:39 +0530 Subject: [PATCH 04/11] fix(playground): keep script-set headers over the auth config --- .../src/runner/RequestExecutor.spec.ts | 65 +++++++++++++++++++ .../src/runner/RequestExecutor.ts | 10 ++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/bruno-api-docs/src/runner/RequestExecutor.spec.ts b/packages/bruno-api-docs/src/runner/RequestExecutor.spec.ts index 67791d82..d9761087 100644 --- a/packages/bruno-api-docs/src/runner/RequestExecutor.spec.ts +++ b/packages/bruno-api-docs/src/runner/RequestExecutor.spec.ts @@ -61,6 +61,71 @@ describe('RequestExecutor', () => { 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); + + 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); + + 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); + + 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({ diff --git a/packages/bruno-api-docs/src/runner/RequestExecutor.ts b/packages/bruno-api-docs/src/runner/RequestExecutor.ts index 7d3bc722..02b9ef96 100644 --- a/packages/bruno-api-docs/src/runner/RequestExecutor.ts +++ b/packages/bruno-api-docs/src/runner/RequestExecutor.ts @@ -131,22 +131,26 @@ export class RequestExecutor { return headers; } + // Existing headers win so pre-request scripts can set their own auth (desktop parity). private setAuthHeaders(headers: Record, 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; } } From 47dcec3c0371dc5ab7ef2823e752ff612165dc71 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Sun, 23 Aug 2026 21:45:42 +0530 Subject: [PATCH 05/11] test(playground): add script library coverage --- .../tests/playground/script-execution.spec.ts | 32 +++++++ .../bruno-api-docs/src/sampleCollection.ts | 15 +++- .../scripting/sandbox/quickjs/lib/jwt.spec.ts | 87 +++++++++++++++++++ .../sandbox/quickjs/library-parity.spec.ts | 82 +++++++++++++++++ 4 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 packages/bruno-api-docs/e2e/tests/playground/script-execution.spec.ts create mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts create mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/library-parity.spec.ts diff --git a/packages/bruno-api-docs/e2e/tests/playground/script-execution.spec.ts b/packages/bruno-api-docs/e2e/tests/playground/script-execution.spec.ts new file mode 100644 index 00000000..017c9830 --- /dev/null +++ b/packages/bruno-api-docs/e2e/tests/playground/script-execution.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from '../../playwright'; + +const DESKTOP = { width: 1280, height: 900 }; + +test.describe('playground script execution', () => { + test.use({ viewport: DESKTOP }); + + test('runs collection scripts using the safe-mode libraries on Send', async ({ page, playground }) => { + await page.route('**/billing/customers', (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([{ id: 1, name: 'Ada' }]) + }) + ); + + await page.goto('/#/?pg=1&dock=bottom'); + await expect(playground.runner).toBeVisible(); + + const playgroundSidebar = page.getByTestId('playground-sidebar'); + await playgroundSidebar.getByRole('button', { name: 'billing' }).click(); + await playgroundSidebar.getByRole('button', { name: 'customers' }).click(); + await playgroundSidebar.getByRole('button', { name: 'Get All Customers' }).click(); + + await page.getByRole('button', { name: 'Send' }).click(); + + await page.getByTestId('response-tabs-more').click(); + await page.getByTestId('response-tabs-more-tests').click(); + await expect(page.getByText('safe-mode libraries work in the playground')).toBeVisible(); + await expect(page.getByText('customers folder scripts ran before the request')).toBeVisible(); + }); +}); diff --git a/packages/bruno-api-docs/src/sampleCollection.ts b/packages/bruno-api-docs/src/sampleCollection.ts index 7a484f16..9d63083d 100644 --- a/packages/bruno-api-docs/src/sampleCollection.ts +++ b/packages/bruno-api-docs/src/sampleCollection.ts @@ -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\` @@ -651,6 +653,17 @@ items: test('customers folder scripts ran before the request', () => { expect(bru.getVar('execChain')).to.contain('F2(customers)'); }); + const moment = require('moment'); + const CryptoJS = require('crypto-js'); + const { v4: uuidv4, validate: uuidValidate } = require('uuid'); + const jwtLib = require('jsonwebtoken'); + test('safe-mode libraries work in the playground', () => { + expect(moment('2026-01-02').format('YYYY-MM-DD')).to.equal('2026-01-02'); + expect(CryptoJS.SHA256('abc').toString()).to.have.lengthOf(64); + expect(uuidValidate(uuidv4())).to.equal(true); + const token = jwtLib.sign({ ok: true }, 'e2e-secret', { noTimestamp: true }); + expect(jwtLib.verify(token, 'e2e-secret').ok).to.equal(true); + }); items: - info: name: Get All Customers diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts new file mode 100644 index 00000000..a450497c --- /dev/null +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from 'vitest'; +import jwt from './jwt'; + +const SECRET = 'test-secret'; + +describe('jwt (sandbox jsonwebtoken replacement)', () => { + it('signs and verifies a payload with each HS algorithm', () => { + for (const algorithm of ['HS256', 'HS384', 'HS512']) { + const token = jwt.sign({ userId: 42 }, SECRET, { algorithm, noTimestamp: true }); + expect(jwt.verify(token, SECRET)).toEqual({ userId: 42 }); + } + }); + + it('produces the exact token bytes for a fixed payload', () => { + const token = jwt.sign({ userId: 1, iat: 1700000000 }, 'shared-secret'); + expect(token).toBe( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcwMDAwMDAwMH0.sFf3AhH_8eWxHEXov2j3nvjgg4XgTSD4Mc4Ao3saMhs' + ); + }); + + it('rejects a tampered token with the real library message', () => { + const token = jwt.sign({ a: 1 }, SECRET, { noTimestamp: true }); + const tampered = token.slice(0, -4) + 'AAAA'; + expect(() => jwt.verify(tampered, SECRET)).toThrowError('invalid signature'); + }); + + it('expires tokens and honours clockTimestamp', () => { + const token = jwt.sign({ a: 1, iat: 1700000000 }, SECRET, { expiresIn: '1h' }); + expect(jwt.verify(token, SECRET, { clockTimestamp: 1700000000 + 3599 }).a).toBe(1); + expect(() => jwt.verify(token, SECRET, { clockTimestamp: 1700000000 + 3601 })).toThrowError('jwt expired'); + }); + + it('honours notBefore with the real library message', () => { + const token = jwt.sign({ a: 1, iat: 1700000000 }, SECRET, { notBefore: '10m' }); + expect(() => jwt.verify(token, SECRET, { clockTimestamp: 1700000000 + 60 })).toThrowError('jwt not active'); + expect(jwt.verify(token, SECRET, { clockTimestamp: 1700000000 + 601 }).a).toBe(1); + }); + + it('parses expiresIn duration strings like the ms grammar', () => { + const token = jwt.sign({ a: 1, iat: 1700000000 }, SECRET, { expiresIn: '2 days' }); + expect((jwt.decode(token) as any).exp).toBe(1700000000 + 2 * 24 * 60 * 60); + }); + + it('validates audience, issuer and maxAge with real library messages', () => { + const token = jwt.sign({ a: 1, aud: ['web', 'mobile'], iss: 'bruno', iat: 1700000000 }, SECRET); + expect(jwt.verify(token, SECRET, { audience: 'mobile', clockTimestamp: 1700000000 }).iss).toBe('bruno'); + expect(() => jwt.verify(token, SECRET, { audience: 'desktop', clockTimestamp: 1700000000 })) + .toThrowError('jwt audience invalid. expected: desktop'); + expect(() => jwt.verify(token, SECRET, { issuer: 'other', clockTimestamp: 1700000000 })) + .toThrowError('jwt issuer invalid. expected: other'); + expect(() => jwt.verify(token, SECRET, { maxAge: '30m', clockTimestamp: 1700000000 + 3600 })) + .toThrowError('maxAge exceeded'); + }); + + it('decodes without verification, including complete mode and malformed input', () => { + const token = jwt.sign({ a: 1 }, SECRET, { noTimestamp: true }); + expect(jwt.decode(token)).toEqual({ a: 1 }); + expect((jwt.decode(token, { complete: true }) as any).header).toEqual({ alg: 'HS256', typ: 'JWT' }); + expect(jwt.decode('not-a-token')).toBeNull(); + }); + + it('supports non-string secrets via byte arrays', () => { + const secretBytes = [112, 97, 115, 115]; + const token = jwt.sign({ a: 1 }, secretBytes, { noTimestamp: true }); + expect(jwt.verify(token, 'pass')).toEqual({ a: 1 }); + }); + + it('supports the node callback form', () => { + let result: any; + jwt.sign({ b: 2 }, SECRET, { noTimestamp: true }, (signError: any, token: string) => { + expect(signError).toBeNull(); + jwt.verify(token, SECRET, (verifyError: any, decoded: any) => { + expect(verifyError).toBeNull(); + result = decoded; + }); + }); + expect(result).toEqual({ b: 2 }); + }); + + it('rejects the alg=none attack token and asymmetric algorithms', () => { + const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url'); + const payload = Buffer.from(JSON.stringify({ a: 1 })).toString('base64url'); + expect(() => jwt.verify(`${header}.${payload}.`, SECRET)).toThrowError('jwt signature is required'); + expect(() => jwt.sign({ a: 1 }, 'key', { algorithm: 'RS256' })) + .toThrowError(/RS256 is not supported in the docs playground/); + }); +}); diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/library-parity.spec.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/library-parity.spec.ts new file mode 100644 index 00000000..9e0f78c0 --- /dev/null +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/library-parity.spec.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { newQuickJSWASMModule } from 'quickjs-emscripten'; +import addCryptoUtilsShimToContext from './shims/lib/crypto-utils'; +import addAxiosShimToContext from './shims/lib/axios'; +import addLocalModuleShimToContext from './shims/local-module'; +import { getRequireCode } from './shims/require'; +import { getBundledCode } from './bundled-libraries.iife.js'; + +const DESKTOP_SAFE_MODE_MODULES = [ + 'ajv', 'ajv-formats', 'atob', 'axios', 'btoa', 'buffer', 'chai', 'crypto-js', + 'jsonwebtoken', 'moment', 'nanoid', 'path', 'tv4', 'uuid' +]; + +let vm: any; + +const inVm = (expression: string) => { + const result = vm.evalCode(expression); + if (result.error) { + const error = vm.dump(result.error); + result.error.dispose(); + throw new Error(error.message); + } + const value = vm.dump(result.value); + result.value.dispose(); + return value; +}; + +const errorMessageOf = (expression: string) => + inVm(`(() => { try { ${expression}; return 'NO-THROW'; } catch (e) { return e.message; } })()`); + +describe('sandbox library parity with desktop safe mode', () => { + beforeAll(async () => { + const module = await newQuickJSWASMModule(); + vm = module.newContext(); + addCryptoUtilsShimToContext(vm); + addLocalModuleShimToContext(vm); + const boot = vm.evalCode( + `(${getBundledCode.toString()})(); ${getRequireCode()}; ` + + `globalThis.bru = { cwd: () => '' }; ` + + `globalThis.console = { log() {}, debug() {}, info() {}, warn() {}, error() {} };` + ); + expect(boot.error).toBeUndefined(); + boot.value.dispose(); + addAxiosShimToContext(vm); + }); + + it('exposes exactly the 14 desktop safe-mode modules', () => { + expect(inVm('Object.keys(globalThis.requireObject).sort()')).toEqual(DESKTOP_SAFE_MODE_MODULES); + }); + + it('exposes the desktop safe-mode globals', () => { + const globals = ['expect', 'assert', 'moment', 'btoa', 'atob', 'Buffer', 'tv4', 'Ajv', 'addFormats', 'crypto', 'axios', 'jwt', 'path', 'require']; + for (const name of globals) { + expect(inVm(`typeof globalThis['${name}']`), name).not.toBe('undefined'); + } + }); + + it('every module does real work inside the VM', () => { + expect(inVm(`require('moment')('2026-08-20T10:00:00Z').utc().format('YYYY-MM-DD')`)).toBe('2026-08-20'); + expect(inVm(`require('crypto-js').SHA256('abc').toString()`)).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); + expect(inVm(`require('uuid').validate(require('uuid').v4())`)).toBe(true); + expect(inVm(`require('nanoid').nanoid(10).length`)).toBe(10); + expect(inVm(`require('buffer').Buffer.from('hello').toString('base64')`)).toBe('aGVsbG8='); + expect(inVm(`require('btoa')('hello')`)).toBe('aGVsbG8='); + expect(inVm(`require('atob')('aGVsbG8=')`)).toBe('hello'); + expect(inVm(`require('tv4').validate({ a: 1 }, { type: 'object' })`)).toBe(true); + expect(inVm(`new (require('ajv'))().compile({ type: 'number' })(5)`)).toBe(true); + expect(inVm(`(() => { const Ajv = require('ajv'); const ajv = new Ajv(); require('ajv-formats')(ajv); return ajv.compile({ type: 'string', format: 'email' })('a@b.co'); })()`)).toBe(true); + expect(inVm(`require('path').resolve('/a/b', '../c')`)).toBe('/a/c'); + expect(inVm(`(() => { const { expect } = require('chai'); expect(1).to.eql(1); return 'ok'; })()`)).toBe('ok'); + expect(inVm(`(() => { const jwtLib = require('jsonwebtoken'); return jwtLib.verify(jwtLib.sign({ u: 1 }, 's', { noTimestamp: true }), 's').u; })()`)).toBe(1); + expect(inVm(`typeof require('axios').get`)).toBe('function'); + }); + + it('gives explanatory errors for developer-mode-only and node builtin modules', () => { + expect(errorMessageOf(`require('lodash')`)).toContain('only available in the Bruno desktop app\'s developer mode'); + expect(errorMessageOf(`require('fs')`)).toContain('is a Node.js builtin'); + expect(errorMessageOf(`require('node:fs')`)).toContain('is a Node.js builtin'); + expect(errorMessageOf(`require('./helper.js')`)).toContain('Local file require is not available in the docs playground'); + expect(errorMessageOf(`require('left-pad-9000')`)).toBe('Cannot find module left-pad-9000'); + }); +}); From 7ddd5c0b3f8812a2d76fe57c2b6819f68f031bc6 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Mon, 24 Aug 2026 00:29:46 +0530 Subject: [PATCH 06/11] chore(playground): restore uuid type declarations for the build --- package-lock.json | 8 ++++++++ packages/bruno-api-docs/package.json | 19 ++++++++++--------- .../src/runner/RequestExecutor.ts | 1 - 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86cbc6a3..41680d5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3140,6 +3140,13 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.65.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", @@ -13299,6 +13306,7 @@ "@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", "concurrently": "^9.1.2", diff --git a/packages/bruno-api-docs/package.json b/packages/bruno-api-docs/package.json index c1c4b0e6..7cd31d41 100644 --- a/packages/bruno-api-docs/package.json +++ b/packages/bruno-api-docs/package.json @@ -78,12 +78,8 @@ "btoa": "^1.2.1", "buffer": "^6.0.3", "chai": "~5.3.3", - "crypto-js": "^4.2.0", - "moment": "^2.30.1", - "path-browserify": "^1.0.1", - "tv4": "^1.3.0", - "uuid": "^10.0.0", "codemirror": "^6.0.2", + "crypto-js": "^4.2.0", "fast-json-format": "~0.4.0", "fuse.js": "^7.5.0", "js-yaml": "^4.1.0", @@ -91,9 +87,11 @@ "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.32.0", @@ -106,6 +104,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": { @@ -115,16 +115,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/path-browserify": "^1.0.3", "@types/prismjs": "^1.26.3", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", - "@types/atob": "^2.1.4", - "@types/btoa": "^1.2.5", - "@types/crypto-js": "^4.2.2", - "@types/path-browserify": "^1.0.3", "@types/tv4": "^1.2.33", + "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "concurrently": "^9.1.2", diff --git a/packages/bruno-api-docs/src/runner/RequestExecutor.ts b/packages/bruno-api-docs/src/runner/RequestExecutor.ts index 02b9ef96..9b8ae1ce 100644 --- a/packages/bruno-api-docs/src/runner/RequestExecutor.ts +++ b/packages/bruno-api-docs/src/runner/RequestExecutor.ts @@ -131,7 +131,6 @@ export class RequestExecutor { return headers; } - // Existing headers win so pre-request scripts can set their own auth (desktop parity). private setAuthHeaders(headers: Record, auth: any) { const hasHeader = (name: string) => Object.keys(headers).some((key) => key.toLowerCase() === name.toLowerCase()); From 88a241188cf8e8a82915fd64e7a6fc78e0bccefe Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Wed, 26 Aug 2026 18:01:32 +0530 Subject: [PATCH 07/11] fix(playground): harden the script sandbox per review - require absolute http(s) urls and omit credentials in the axios shim - reject asymmetric keys and missing secrets in the jwt helper - keep the crypto shim working under minified builds - replace local-file require with a clear error and remove the unused loader - strip __proto__ when marshalling host values into the sandbox --- .../src/scripting/sandbox/quickjs/index.ts | 7 +++-- .../src/scripting/sandbox/quickjs/lib/jwt.ts | 21 +++++++++++-- .../sandbox/quickjs/shims/lib/axios.ts | 31 ++++++++++++++++--- .../sandbox/quickjs/shims/lib/crypto-utils.ts | 10 ++++-- .../sandbox/quickjs/shims/lib/utils.ts | 9 +++--- .../sandbox/quickjs/shims/local-module.ts | 25 --------------- .../sandbox/quickjs/shims/require.ts | 31 ++++++++----------- .../scripting/sandbox/quickjs/utils/index.ts | 3 ++ 8 files changed, 79 insertions(+), 58 deletions(-) delete mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/local-module.ts diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/index.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/index.ts index 8483cdfd..79365e7d 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/index.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/index.ts @@ -8,7 +8,6 @@ import addAxiosShimToContext from './shims/lib/axios'; import { newQuickJSWASMModule, memoizePromiseFactory } from 'quickjs-emscripten'; import { marshallToVm } from './utils'; import { getBundledCode } from './bundled-libraries.iife.js'; -import addLocalModuleShimToContext from './shims/local-module'; import { getRequireCode } from './shims/require'; let QuickJSSyncContext: any; @@ -103,9 +102,11 @@ const executeQuickJsVmAsync = async ({ const vm = module.newContext(); addCryptoUtilsShimToContext(vm); - addLocalModuleShimToContext(vm); - const bundledCode = getBundledCode?.toString() || ''; + if (typeof getBundledCode !== 'function') { + throw new Error('Sandbox library bundle is missing; run build:lib-bundle before executing scripts.'); + } + const bundledCode = getBundledCode.toString(); const bootResult = vm.evalCode( ` diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts index fef21819..08335fc0 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts @@ -53,6 +53,7 @@ const toHmacSecret = (secret: any): CryptoJS.lib.WordArray | string => { if (secret && typeof secret === 'object' && typeof secret.length === 'number') { const bytes = Array.from(secret as ArrayLike); const words: number[] = []; + // crypto-js stores bytes packed four-per-word, most significant byte first. for (let i = 0; i < bytes.length; i++) { words[i >>> 2] = (words[i >>> 2] || 0) | (bytes[i] << (24 - (i % 4) * 8)); } @@ -65,8 +66,18 @@ const base64UrlFromWordArray = (words: CryptoJS.lib.WordArray): string => { return words.toString(CryptoJS.enc.Base64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }; +const base64UrlEncodeString = (value: string): string => { + return base64UrlFromWordArray(CryptoJS.enc.Utf8.parse(value)); +}; + const base64UrlEncodeJson = (value: unknown): string => { - return base64UrlFromWordArray(CryptoJS.enc.Utf8.parse(JSON.stringify(value))); + return base64UrlEncodeString(JSON.stringify(value)); +}; + +const assertSymmetricSecret = (secret: unknown) => { + if (typeof secret === 'string' && secret.includes('-----BEGIN')) { + throw new Error('an asymmetric key was provided for an HMAC (HS) algorithm'); + } }; const base64UrlDecodeToString = (segment: string): string => { @@ -130,6 +141,7 @@ const signSync = (payload: any, secret: any, options: any): string => { if (secret === undefined || secret === null || secret === '') { throw new Error('secretOrPrivateKey must have a value'); } + assertSymmetricSecret(secret); const isObjectPayload = typeof payload === 'object' && payload !== null; const claims: any = isObjectPayload && options?.mutatePayload !== true ? { ...payload } : payload; @@ -151,7 +163,8 @@ const signSync = (payload: any, secret: any, options: any): string => { } const header = { alg: algorithm, typ: 'JWT', ...(options?.header || {}) }; - const signingInput = `${base64UrlEncodeJson(header)}.${base64UrlEncodeJson(claims)}`; + const encodedPayload = typeof claims === 'string' ? base64UrlEncodeString(claims) : base64UrlEncodeJson(claims); + const signingInput = `${base64UrlEncodeJson(header)}.${encodedPayload}`; const signature = base64UrlFromWordArray(hmac(signingInput, toHmacSecret(secret))); return `${signingInput}.${signature}`; }; @@ -192,6 +205,10 @@ const verifySync = (token: any, secret: any, options: any): any => { if (typeof token !== 'string' || token.split('.').length !== 3) { throw new Error('jwt malformed'); } + if (secret === undefined || secret === null || secret === '') { + throw new Error('secret or public key must be provided'); + } + assertSymmetricSecret(secret); const [headerSegment, payloadSegment, signatureSegment] = token.split('.'); if (signatureSegment === '') { diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/axios.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/axios.ts index 06ddbd34..e773a2e2 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/axios.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/axios.ts @@ -3,6 +3,7 @@ import { marshallToVm } from '../../utils'; const METHODS = ['get', 'post', 'put', 'delete', 'patch']; const METHODS_WITH_BODY = new Set(['post', 'put', 'patch']); +const UNSUPPORTED_METHODS = ['head', 'options', 'request', 'create', 'all']; interface AxiosShimConfig { url?: string; @@ -74,8 +75,11 @@ const parseResponseData = async (response: Response) => { }; const performAxiosRequest = async (config: AxiosShimConfig) => { - if (typeof config.url !== 'string' || config.url.trim() === '') { - throw buildAxiosError({ message: `Invalid URL: ${String(config.url)}`, code: 'ERR_INVALID_URL' }, config); + if (typeof config.url !== 'string' || !/^https?:\/\//i.test(config.url.trim())) { + throw buildAxiosError( + { message: `Only absolute http(s) URLs are supported here, received: ${String(config.url)}`, code: 'ERR_INVALID_URL' }, + config + ); } const headers: Record = { ...(config.headers || {}) }; @@ -100,6 +104,9 @@ const performAxiosRequest = async (config: AxiosShimConfig) => { const fetchOptions: RequestInit = { method: (config.method || 'get').toUpperCase(), headers, + // Never send the docs site's own cookies, so a published script cannot make + // authenticated calls to the host origin on the reader's behalf. + credentials: 'omit', ...(body !== undefined && { body }), ...(config.timeout && { signal: AbortSignal.timeout(config.timeout) }) }; @@ -115,6 +122,7 @@ const performAxiosRequest = async (config: AxiosShimConfig) => { { message: 'Network Error', code: 'ERR_NETWORK', + cause: `${err?.name || 'Error'}: ${err?.message || String(err)}`, hint: 'The browser blocked or failed the request. If the API works in this Bruno app, the server may not allow cross-origin (CORS) requests from this docs site.' }, config @@ -143,7 +151,13 @@ const performAxiosRequest = async (config: AxiosShimConfig) => { ); } - return { status: response.status, headers: responseHeaders, data }; + return { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + data, + config: { url: config.url, method: config.method, headers: config.headers, data: config.data } + }; }; const addAxiosShimToContext = (vm: any) => { @@ -168,14 +182,23 @@ const addAxiosShimToContext = (vm: any) => { registerAxiosFunction('__bruno__axios', null); METHODS.forEach((method) => registerAxiosFunction(`__bruno__axios__${method}`, method)); - vm.evalCode(` + const bootResult = vm.evalCode(` globalThis.axios = __bruno__axios; ${METHODS.map((method) => `globalThis.axios.${method} = __bruno__axios__${method};`).join('\n')} + ${UNSUPPORTED_METHODS.map((method) => `globalThis.axios.${method} = () => { + throw new Error("axios.${method} is not supported in the docs playground; use axios(config) or axios.get/post/put/patch/delete."); + };`).join('\n')} globalThis.requireObject = { ...globalThis.requireObject, axios: globalThis.axios, }; `); + if (bootResult.error) { + const error = vm.dump(bootResult.error); + bootResult.error.dispose(); + throw new Error(`Failed to install axios shim: ${error?.message || String(error)}`); + } + bootResult.value.dispose(); }; export default addAxiosShimToContext; diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/crypto-utils.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/crypto-utils.ts index 70fab289..a75da936 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/crypto-utils.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/crypto-utils.ts @@ -64,8 +64,8 @@ const addCryptoUtilsShimToContext = (vm: any) => { randomBytesHandle.dispose(); getRandomValuesHandle.dispose(); - vm.evalCode(` - ${serializeTypedArray.toString()} + const bootResult = vm.evalCode(` + const serializeTypedArray = ${serializeTypedArray.toString()}; const cryptoModule = { randomBytes: function(size) { @@ -81,6 +81,12 @@ const addCryptoUtilsShimToContext = (vm: any) => { globalThis.crypto = cryptoModule; `); + if (bootResult.error) { + const error = vm.dump(bootResult.error); + bootResult.error.dispose(); + throw new Error(`Failed to install crypto shim: ${error?.message || String(error)}`); + } + bootResult.value.dispose(); }; export default addCryptoUtilsShimToContext; diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts index f8d1ed48..9d24b74f 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts @@ -12,11 +12,12 @@ const ALLOWED_TYPED_ARRAYS = new Set([ 'BigUint64Array' ]); -function serializeTypedArray(ta: any) { +// Stringified and injected into the sandbox, so it must not reference anything outside its own body. +function serializeTypedArray(typedArray: any) { return { - type: ta.constructor.name, - array: Array.from(ta), - length: ta.length + type: typedArray.constructor.name, + array: Array.from(typedArray), + length: typedArray.length }; } diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/local-module.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/local-module.ts deleted file mode 100644 index f6df759d..00000000 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/local-module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { marshallToVm } from '../utils'; - -type LocalModuleResolver = (modulePath: string) => string | null; - -let localModuleResolver: LocalModuleResolver = () => null; - -export const setLocalModuleResolver = (resolver: LocalModuleResolver) => { - localModuleResolver = resolver; -}; - -const addLocalModuleShimToContext = (vm: any) => { - const loadLocalModule = vm.newFunction('__brunoLoadLocalModule', (pathHandle: any) => { - const modulePath = String(vm.dump(pathHandle)); - const source = localModuleResolver(modulePath); - if (source === null || source === undefined) { - throw vm.newError( - `Local file require is not available in the docs playground (tried to load '${modulePath}'). ` + 'Only the built-in safe-mode libraries can be required here.' - ); - } - return marshallToVm(source, vm); - }); - loadLocalModule.consume((handle: any) => vm.setProp(vm.global, '__brunoLoadLocalModule', handle)); -}; - -export default addLocalModuleShimToContext; diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/require.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/require.ts index 0d5c58be..f66d1db5 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/require.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/require.ts @@ -1,6 +1,6 @@ const NODE_BUILTIN_MODULES = [ 'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns', 'events', - 'fs', 'http', 'http2', 'https', 'net', 'os', 'perf_hooks', 'process', 'querystring', + 'fs', 'http', 'http2', 'https', 'net', 'os', 'path', 'perf_hooks', 'process', 'querystring', 'readline', 'stream', 'string_decoder', 'timers', 'tls', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib' ]; @@ -17,34 +17,29 @@ export const getRequireCode = () => ` return lib; } - const collectionCwd = bru.cwd(); - const isModuleAPath = (module) => (module?.startsWith('.') || (!!collectionCwd && module?.startsWith?.(collectionCwd))); - if (isModuleAPath(mod)) { - const localModuleCode = globalThis.__brunoLoadLocalModule(mod); - (function () { - const initModuleExportsCode = "const module = { exports: {} };"; - const copyModuleExportsCode = "\\n;globalThis.requireObject[mod] = module.exports;"; - const patchedRequire = - "\\n;" + - "let require = (subModule) => isModuleAPath(subModule) ? globalThis.require(path.resolve(bru.cwd(), mod, '..', subModule)) : globalThis.require(subModule)" + - "\\n;"; - eval(initModuleExportsCode + patchedRequire + localModuleCode + copyModuleExportsCode); - })(); - return globalThis.requireObject[mod]; + if (mod?.startsWith?.('.') || mod?.startsWith?.('/')) { + throw new Error( + "Local file require ('" + mod + "') is not available in the docs playground; " + + 'only the built-in safe-mode libraries can be required here.' + ); } const bareName = mod?.startsWith?.('node:') ? mod.slice(5) : mod; - if (${JSON.stringify(NODE_BUILTIN_MODULES)}.includes(bareName)) { + const nodeBuiltins = ${JSON.stringify(NODE_BUILTIN_MODULES)}; + if (nodeBuiltins.includes(bareName)) { + if (globalThis.requireObject[bareName]) { + return globalThis.requireObject[bareName]; + } throw new Error( "'" + mod + "' is a Node.js builtin and is not available in the docs playground; " + "it requires the Bruno desktop app's developer mode." ); } if (${JSON.stringify(DEVELOPER_MODE_LIBRARIES)}.includes(bareName)) { + const available = Object.keys(globalThis.requireObject).sort().join(', '); throw new Error( "'" + mod + "' is only available in the Bruno desktop app's developer mode; " + - "the docs playground supports the safe-mode library set (chai, moment, uuid, nanoid, axios, " + - "crypto-js, jsonwebtoken, tv4, ajv, ajv-formats, path, buffer, btoa, atob)." + "the docs playground supports the safe-mode library set (" + available + ")." ); } throw new Error("Cannot find module " + mod); diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/utils/index.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/utils/index.ts index a587615d..aa7d8489 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/utils/index.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/utils/index.ts @@ -21,6 +21,9 @@ const marshallToVm = (value: any, vm: any) => { } else { const obj = vm.newObject(); for (const key in value) { + if (key === '__proto__') { + continue; + } vm.setProp(obj, key, marshallToVm(value[key], vm)); } return obj; From 632ce24c145aa962a5c230cf1094dde225c2d057 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Wed, 26 Aug 2026 18:01:43 +0530 Subject: [PATCH 08/11] test(playground): extend sandbox coverage and simplify the script e2e - add jwt guard, marshalling and node: builtin parity cases - rewrite the script-execution e2e to a self-contained tests script - drop the now-unused sample collection script --- .../tests/playground/script-execution.spec.ts | 58 ++++++++++++++----- .../bruno-api-docs/src/sampleCollection.ts | 11 ---- .../scripting/sandbox/quickjs/lib/jwt.spec.ts | 42 +++++++++++--- .../sandbox/quickjs/library-parity.spec.ts | 9 +-- .../sandbox/quickjs/utils/index.spec.ts | 36 ++++++++++++ 5 files changed, 117 insertions(+), 39 deletions(-) create mode 100644 packages/bruno-api-docs/src/scripting/sandbox/quickjs/utils/index.spec.ts diff --git a/packages/bruno-api-docs/e2e/tests/playground/script-execution.spec.ts b/packages/bruno-api-docs/e2e/tests/playground/script-execution.spec.ts index 017c9830..b435b366 100644 --- a/packages/bruno-api-docs/e2e/tests/playground/script-execution.spec.ts +++ b/packages/bruno-api-docs/e2e/tests/playground/script-execution.spec.ts @@ -1,32 +1,58 @@ import { test, expect } from '../../playwright'; +import type { Page } from '@playwright/test'; +import type { CodeEditorComponent } from '../../components/code-editor/code-editor.component'; -const DESKTOP = { width: 1280, height: 900 }; +const LIBRARY_TESTS_SCRIPT = ` +const moment = require('moment'); +const CryptoJS = require('crypto-js'); +const { v4, validate } = require('uuid'); +const { nanoid } = require('nanoid'); +const jwt = require('jsonwebtoken'); + +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.have.lengthOf(64); + expect(validate(v4())).to.equal(true); + expect(nanoid(10)).to.have.lengthOf(10); +}); + +test('jsonwebtoken round-trips a signed token', function () { + const token = jwt.sign({ userId: 7 }, 'secret', { expiresIn: '1h' }); + expect(jwt.verify(token, 'secret').userId).to.equal(7); +}); +`; + +const setEditorScript = async (page: Page, editor: CodeEditorComponent, script: string): Promise => { + await editor.focus(); + await page.keyboard.press('ControlOrMeta+a'); + await page.keyboard.insertText(script); +}; test.describe('playground script execution', () => { - test.use({ viewport: DESKTOP }); + test.use({ viewport: { width: 1280, height: 900 } }); - test('runs collection scripts using the safe-mode libraries on Send', async ({ page, playground }) => { - await page.route('**/billing/customers', (route) => + 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, - contentType: 'application/json', - body: JSON.stringify([{ id: 1, name: 'Ada' }]) + 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 expect(playground.runner).toBeVisible(); + await playground.openSidebarItem('get users'); - const playgroundSidebar = page.getByTestId('playground-sidebar'); - await playgroundSidebar.getByRole('button', { name: 'billing' }).click(); - await playgroundSidebar.getByRole('button', { name: 'customers' }).click(); - await playgroundSidebar.getByRole('button', { name: 'Get All Customers' }).click(); + await playground.selectTab('tests'); + await setEditorScript(page, playground.testsEditor, LIBRARY_TESTS_SCRIPT); - await page.getByRole('button', { name: 'Send' }).click(); + await responsePane.send(); + await responsePane.switchToTab('tests'); - await page.getByTestId('response-tabs-more').click(); - await page.getByTestId('response-tabs-more-tests').click(); - await expect(page.getByText('safe-mode libraries work in the playground')).toBeVisible(); - await expect(page.getByText('customers folder scripts ran before the request')).toBeVisible(); + await expect(page.getByText(/Passed: [1-9]\d*, Failed: 0/).first()).toBeVisible(); + await expect(page.getByText(/Failed: [1-9]/)).toHaveCount(0); }); }); diff --git a/packages/bruno-api-docs/src/sampleCollection.ts b/packages/bruno-api-docs/src/sampleCollection.ts index 9d63083d..28b13e5a 100644 --- a/packages/bruno-api-docs/src/sampleCollection.ts +++ b/packages/bruno-api-docs/src/sampleCollection.ts @@ -653,17 +653,6 @@ items: test('customers folder scripts ran before the request', () => { expect(bru.getVar('execChain')).to.contain('F2(customers)'); }); - const moment = require('moment'); - const CryptoJS = require('crypto-js'); - const { v4: uuidv4, validate: uuidValidate } = require('uuid'); - const jwtLib = require('jsonwebtoken'); - test('safe-mode libraries work in the playground', () => { - expect(moment('2026-01-02').format('YYYY-MM-DD')).to.equal('2026-01-02'); - expect(CryptoJS.SHA256('abc').toString()).to.have.lengthOf(64); - expect(uuidValidate(uuidv4())).to.equal(true); - const token = jwtLib.sign({ ok: true }, 'e2e-secret', { noTimestamp: true }); - expect(jwtLib.verify(token, 'e2e-secret').ok).to.equal(true); - }); items: - info: name: Get All Customers diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts index a450497c..55d935f7 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts @@ -66,15 +66,23 @@ describe('jwt (sandbox jsonwebtoken replacement)', () => { }); it('supports the node callback form', () => { - let result: any; - jwt.sign({ b: 2 }, SECRET, { noTimestamp: true }, (signError: any, token: string) => { - expect(signError).toBeNull(); - jwt.verify(token, SECRET, (verifyError: any, decoded: any) => { - expect(verifyError).toBeNull(); - result = decoded; - }); + let signError: any = 'not-called'; + let signedToken: string | undefined; + jwt.sign({ b: 2 }, SECRET, { noTimestamp: true }, (error: any, token: string) => { + signError = error; + signedToken = token; }); - expect(result).toEqual({ b: 2 }); + expect(signError).toBeNull(); + expect(typeof signedToken).toBe('string'); + + let verifyError: any = 'not-called'; + let decodedPayload: any; + jwt.verify(signedToken as string, SECRET, (error: any, decoded: any) => { + verifyError = error; + decodedPayload = decoded; + }); + expect(verifyError).toBeNull(); + expect(decodedPayload).toEqual({ b: 2 }); }); it('rejects the alg=none attack token and asymmetric algorithms', () => { @@ -84,4 +92,22 @@ describe('jwt (sandbox jsonwebtoken replacement)', () => { expect(() => jwt.sign({ a: 1 }, 'key', { algorithm: 'RS256' })) .toThrowError(/RS256 is not supported in the docs playground/); }); + + it('throws when verify is called without a secret', () => { + const token = jwt.sign({ a: 1 }, SECRET, { noTimestamp: true }); + expect(() => jwt.verify(token, undefined as any)).toThrowError('secret or public key must be provided'); + }); + + it('rejects an asymmetric key handed to an HMAC algorithm (RS/HS confusion)', () => { + const pem = '-----BEGIN PUBLIC KEY-----\nMFkw\n-----END PUBLIC KEY-----'; + expect(() => jwt.sign({ a: 1 }, pem)).toThrowError('an asymmetric key was provided for an HMAC (HS) algorithm'); + const token = jwt.sign({ a: 1 }, SECRET, { noTimestamp: true }); + expect(() => jwt.verify(token, pem)).toThrowError('an asymmetric key was provided for an HMAC (HS) algorithm'); + }); + + it('signs a string payload verbatim (no JSON quoting)', () => { + const token = jwt.sign('hello', SECRET, { noTimestamp: true }); + expect(jwt.decode(token)).toBe('hello'); + expect(jwt.verify(token, SECRET)).toBe('hello'); + }); }); diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/library-parity.spec.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/library-parity.spec.ts index 9e0f78c0..2d6a74ee 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/library-parity.spec.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/library-parity.spec.ts @@ -2,7 +2,6 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { newQuickJSWASMModule } from 'quickjs-emscripten'; import addCryptoUtilsShimToContext from './shims/lib/crypto-utils'; import addAxiosShimToContext from './shims/lib/axios'; -import addLocalModuleShimToContext from './shims/local-module'; import { getRequireCode } from './shims/require'; import { getBundledCode } from './bundled-libraries.iife.js'; @@ -33,10 +32,8 @@ describe('sandbox library parity with desktop safe mode', () => { const module = await newQuickJSWASMModule(); vm = module.newContext(); addCryptoUtilsShimToContext(vm); - addLocalModuleShimToContext(vm); const boot = vm.evalCode( `(${getBundledCode.toString()})(); ${getRequireCode()}; ` - + `globalThis.bru = { cwd: () => '' }; ` + `globalThis.console = { log() {}, debug() {}, info() {}, warn() {}, error() {} };` ); expect(boot.error).toBeUndefined(); @@ -76,7 +73,11 @@ describe('sandbox library parity with desktop safe mode', () => { expect(errorMessageOf(`require('lodash')`)).toContain('only available in the Bruno desktop app\'s developer mode'); expect(errorMessageOf(`require('fs')`)).toContain('is a Node.js builtin'); expect(errorMessageOf(`require('node:fs')`)).toContain('is a Node.js builtin'); - expect(errorMessageOf(`require('./helper.js')`)).toContain('Local file require is not available in the docs playground'); + expect(errorMessageOf(`require('./helper.js')`)).toContain('Local file require'); + expect(errorMessageOf(`require('./helper.js')`)).toContain('is not available in the docs playground'); + expect(inVm(`typeof require('node:buffer').Buffer`)).toBe('function'); + expect(inVm(`typeof require('node:path').resolve`)).toBe('function'); + expect(errorMessageOf(`require('node:chai')`)).toContain('Cannot find module node:chai'); expect(errorMessageOf(`require('left-pad-9000')`)).toBe('Cannot find module left-pad-9000'); }); }); diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/utils/index.spec.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/utils/index.spec.ts new file mode 100644 index 00000000..b38bc935 --- /dev/null +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/utils/index.spec.ts @@ -0,0 +1,36 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { newQuickJSWASMModule } from 'quickjs-emscripten'; +import { marshallToVm } from './index'; + +let vm: any; + +const dump = (handle: any) => { + const value = vm.dump(handle); + handle.dispose(); + return value; +}; + +describe('marshallToVm', () => { + beforeAll(async () => { + const module = await newQuickJSWASMModule(); + vm = module.newContext(); + }); + + it('marshals primitives, arrays and nested objects', () => { + expect(dump(marshallToVm('x', vm))).toBe('x'); + expect(dump(marshallToVm(42, vm))).toBe(42); + expect(dump(marshallToVm(true, vm))).toBe(true); + expect(dump(marshallToVm([1, 'a', { b: 2 }], vm))).toEqual([1, 'a', { b: 2 }]); + }); + + it('does not carry a hostile __proto__ key onto the sandbox object', () => { + const hostile = JSON.parse('{"__proto__": {"polluted": true}, "safe": 1}'); + const handle = marshallToVm(hostile, vm); + vm.setProp(vm.global, 'marshalled', handle); + handle.dispose(); + + expect(dump(vm.evalCode('marshalled.safe').value)).toBe(1); + expect(dump(vm.evalCode('marshalled.polluted').value)).toBeUndefined(); + expect(dump(vm.evalCode('({}).polluted').value)).toBeUndefined(); + }); +}); From 3ce2ad95b74c9fe71e9aa5752dc9f8e5831dc747 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Wed, 26 Aug 2026 20:45:58 +0530 Subject: [PATCH 09/11] fix(playground): match jsonwebtoken behaviour for string payloads and callbacks - omit typ and reject claim options for string payloads - invoke the callback at most once - add node crypto interop tests for HS signatures --- .../scripting/sandbox/quickjs/lib/jwt.spec.ts | 51 ++++++++++++++++++- .../src/scripting/sandbox/quickjs/lib/jwt.ts | 18 ++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts index 55d935f7..702ca760 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts @@ -106,8 +106,57 @@ describe('jwt (sandbox jsonwebtoken replacement)', () => { }); it('signs a string payload verbatim (no JSON quoting)', () => { - const token = jwt.sign('hello', SECRET, { noTimestamp: true }); + const token = jwt.sign('hello', SECRET); expect(jwt.decode(token)).toBe('hello'); expect(jwt.verify(token, SECRET)).toBe('hello'); }); + + it('omits typ for a string payload and keeps it for an object payload', () => { + const stringHeader = jwt.decode(jwt.sign('hello', SECRET), { complete: true }).header; + expect(stringHeader.typ).toBeUndefined(); + expect(stringHeader.alg).toBe('HS256'); + + const objectHeader = jwt.decode(jwt.sign({ a: 1 }, SECRET), { complete: true }).header; + expect(objectHeader.typ).toBe('JWT'); + }); + + it('rejects claim options for a string payload with the real library messages', () => { + for (const option of ['expiresIn', 'notBefore', 'noTimestamp', 'audience', 'issuer', 'subject', 'jwtid']) { + expect(() => jwt.sign('hello', SECRET, { [option]: option === 'noTimestamp' ? true : '1h' })) + .toThrowError(`invalid ${option} option for string payload`); + } + }); + + it('invokes a throwing callback at most once', () => { + let calls = 0; + const throwingCallback = () => { + calls += 1; + throw new Error('callback boom'); + }; + expect(() => jwt.sign({ a: 1 }, SECRET, { noTimestamp: true }, throwingCallback)).toThrowError('callback boom'); + expect(calls).toBe(1); + }); + + it('produces HS256/384/512 signatures matching node crypto (interop)', async () => { + const { createHmac } = await import('node:crypto'); + const algs: Array<['HS256' | 'HS384' | 'HS512', string]> = [ + ['HS256', 'sha256'], ['HS384', 'sha384'], ['HS512', 'sha512'] + ]; + for (const [alg, nodeAlg] of algs) { + const token = jwt.sign({ userId: 7 }, SECRET, { algorithm: alg, noTimestamp: true }); + const [header, payload, signature] = token.split('.'); + const reference = createHmac(nodeAlg, SECRET).update(`${header}.${payload}`).digest('base64url'); + expect(signature).toBe(reference); + } + }); + + it('signs a string-payload token a strict jws verifier accepts (interop)', async () => { + const { createHmac } = await import('node:crypto'); + const token = jwt.sign('raw-string', SECRET); + const [header, payload, signature] = token.split('.'); + const reference = createHmac('sha256', SECRET).update(`${header}.${payload}`).digest('base64url'); + expect(signature).toBe(reference); + expect(JSON.parse(Buffer.from(header, 'base64url').toString()).typ).toBeUndefined(); + expect(Buffer.from(payload, 'base64url').toString()).toBe('raw-string'); + }); }); diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts index 08335fc0..e668a5ee 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.ts @@ -112,11 +112,14 @@ const withOptionalCallback = (callback: any, run: () => T): T | undefined => if (typeof callback !== 'function') { return run(); } + let result: T; try { - callback(null, run()); + result = run(); } catch (error) { callback(error, undefined); + return undefined; } + callback(null, result); return undefined; }; @@ -144,6 +147,15 @@ const signSync = (payload: any, secret: any, options: any): string => { assertSymmetricSecret(secret); const isObjectPayload = typeof payload === 'object' && payload !== null; + + if (!isObjectPayload) { + const invalidOption = ['expiresIn', 'notBefore', 'noTimestamp', 'audience', 'issuer', 'subject', 'jwtid'] + .find((name) => options?.[name] !== undefined); + if (invalidOption) { + throw new Error(`invalid ${invalidOption} option for string payload`); + } + } + const claims: any = isObjectPayload && options?.mutatePayload !== true ? { ...payload } : payload; if (isObjectPayload) { @@ -162,7 +174,9 @@ const signSync = (payload: any, secret: any, options: any): string => { if (options?.jwtid !== undefined) claims.jti = options.jwtid; } - const header = { alg: algorithm, typ: 'JWT', ...(options?.header || {}) }; + const header = isObjectPayload + ? { alg: algorithm, typ: 'JWT', ...(options?.header || {}) } + : { alg: algorithm, ...(options?.header || {}) }; const encodedPayload = typeof claims === 'string' ? base64UrlEncodeString(claims) : base64UrlEncodeJson(claims); const signingInput = `${base64UrlEncodeJson(header)}.${encodedPayload}`; const signature = base64UrlFromWordArray(hmac(signingInput, toHmacSecret(secret))); From a1d2ec799d43fe9395d371dbe77dfea9f5481f13 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Thu, 27 Aug 2026 01:37:43 +0530 Subject: [PATCH 10/11] test(playground): type the jwt sign result before string ops --- .../src/scripting/sandbox/quickjs/lib/jwt.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts index 702ca760..ffb43ab8 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/lib/jwt.spec.ts @@ -19,7 +19,7 @@ describe('jwt (sandbox jsonwebtoken replacement)', () => { }); it('rejects a tampered token with the real library message', () => { - const token = jwt.sign({ a: 1 }, SECRET, { noTimestamp: true }); + const token = jwt.sign({ a: 1 }, SECRET, { noTimestamp: true }) as string; const tampered = token.slice(0, -4) + 'AAAA'; expect(() => jwt.verify(tampered, SECRET)).toThrowError('invalid signature'); }); @@ -143,7 +143,7 @@ describe('jwt (sandbox jsonwebtoken replacement)', () => { ['HS256', 'sha256'], ['HS384', 'sha384'], ['HS512', 'sha512'] ]; for (const [alg, nodeAlg] of algs) { - const token = jwt.sign({ userId: 7 }, SECRET, { algorithm: alg, noTimestamp: true }); + const token = jwt.sign({ userId: 7 }, SECRET, { algorithm: alg, noTimestamp: true }) as string; const [header, payload, signature] = token.split('.'); const reference = createHmac(nodeAlg, SECRET).update(`${header}.${payload}`).digest('base64url'); expect(signature).toBe(reference); @@ -152,7 +152,7 @@ describe('jwt (sandbox jsonwebtoken replacement)', () => { it('signs a string-payload token a strict jws verifier accepts (interop)', async () => { const { createHmac } = await import('node:crypto'); - const token = jwt.sign('raw-string', SECRET); + const token = jwt.sign('raw-string', SECRET) as string; const [header, payload, signature] = token.split('.'); const reference = createHmac('sha256', SECRET).update(`${header}.${payload}`).digest('base64url'); expect(signature).toBe(reference); From 7a1cb6d992cbe1411f5f3c7b9746118969ed2641 Mon Sep 17 00:00:00 2001 From: Sundram Gupta Date: Thu, 27 Aug 2026 13:45:08 +0530 Subject: [PATCH 11/11] chore(playground): drop redundant comment in the typed-array helper --- .../src/scripting/sandbox/quickjs/shims/lib/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts index 9d24b74f..897bdfa3 100644 --- a/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts +++ b/packages/bruno-api-docs/src/scripting/sandbox/quickjs/shims/lib/utils.ts @@ -12,7 +12,6 @@ const ALLOWED_TYPED_ARRAYS = new Set([ 'BigUint64Array' ]); -// Stringified and injected into the sandbox, so it must not reference anything outside its own body. function serializeTypedArray(typedArray: any) { return { type: typedArray.constructor.name,