Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/bruno-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"cheerio": "^1.0.0",
"crypto-js": "^4.1.1",
"json-query": "^2.2.2",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "3.3.8",
Expand Down
3 changes: 3 additions & 0 deletions packages/bruno-js/src/runtime/script-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const NodeVault = require('node-vault');
const xml2js = require('xml2js');
const cheerio = require('cheerio');
const tv4 = require('tv4');
const jsonwebtoken = require('jsonwebtoken');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');

class ScriptRuntime {
Expand Down Expand Up @@ -185,6 +186,7 @@ class ScriptRuntime {
'node-fetch': fetch,
'crypto-js': CryptoJS,
xml2js: xml2js,
jsonwebtoken,
cheerio,
tv4,
...whitelistedModules,
Expand Down Expand Up @@ -354,6 +356,7 @@ class ScriptRuntime {
'node-fetch': fetch,
'crypto-js': CryptoJS,
'xml2js': xml2js,
jsonwebtoken,
cheerio,
tv4,
...whitelistedModules,
Expand Down
5 changes: 4 additions & 1 deletion packages/bruno-js/src/runtime/test-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const NodeVault = require('node-vault');
const xml2js = require('xml2js');
const cheerio = require('cheerio');
const tv4 = require('tv4');
const jsonwebtoken = require('jsonwebtoken');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');

class TestRuntime {
Expand Down Expand Up @@ -103,7 +104,8 @@ class TestRuntime {
res,
expect: chai.expect,
assert: chai.assert,
__brunoTestResults: __brunoTestResults
__brunoTestResults: __brunoTestResults,
jwt: jsonwebtoken
};

if (onConsoleLog && typeof onConsoleLog === 'function') {
Expand Down Expand Up @@ -174,6 +176,7 @@ class TestRuntime {
'xml2js': xml2js,
cheerio,
tv4,
'jsonwebtoken': jsonwebtoken,
...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined,
'node-vault': NodeVault
Expand Down
2 changes: 2 additions & 0 deletions packages/bruno-js/src/sandbox/quickjs/shims/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ const addAxiosShimToContext = require('./axios');
const addNanoidShimToContext = require('./nanoid');
const addPathShimToContext = require('./path');
const addUuidShimToContext = require('./uuid');
const addJwtShimToContext = require('./jwt');

const addLibraryShimsToContext = async (vm) => {
await addNanoidShimToContext(vm);
await addAxiosShimToContext(vm);
await addUuidShimToContext(vm);
await addPathShimToContext(vm);
await addJwtShimToContext(vm);
};

module.exports = addLibraryShimsToContext;
183 changes: 183 additions & 0 deletions packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
const jwt = require('jsonwebtoken');
const { marshallToVm, invokeFunction } = require('../../utils');

const addJwtShimToContext = async (vm) => {
// --- sign ---
const _jwtSign = vm.newFunction('sign', function (payload, secret, options, callback) {
const nativePayload = vm.dump(payload);
const nativeSecret = vm.dump(secret);

let nativeOptions;
let callbackHandle = callback;
const optionsType = options === undefined ? 'undefined' : vm.typeof(options);
if (optionsType === 'function') {
callbackHandle = options;
nativeOptions = undefined;
} else if (optionsType === 'object' && options !== null) {
nativeOptions = vm.dump(options);
}

// If a callback is provided
if (callbackHandle && vm.typeof(callbackHandle) === 'function') {

let tokenResult;
let hostError;
try {
tokenResult = nativeOptions
? jwt.sign(nativePayload, nativeSecret, nativeOptions)
: jwt.sign(nativePayload, nativeSecret);
} catch (err) {
hostError = err;
}

try {
if (hostError) {
const errVm = vm.newError(hostError.message || String(hostError));
invokeFunction(vm, callbackHandle, [errVm, vm.undefined])
.catch((e) => {
console.warn('[JWT SHIM][sign.cb] callback invocation error:', e);
})
.finally(() => {
errVm.dispose();
callbackHandle.dispose();
});
} else {
const tokenVm = marshallToVm(String(tokenResult), vm);
invokeFunction(vm, callbackHandle, [vm.null, tokenVm])
.catch((e) => {
console.warn('[JWT SHIM][sign.cb] callback invocation error:', e);
})
.finally(() => {
tokenVm.dispose();
callbackHandle.dispose();
});
}
} catch (e) {
console.warn('[JWT SHIM][sign.cb] unexpected error:', e);
callbackHandle.dispose();
}

return vm.undefined;
}

try {
const token = nativeOptions
? jwt.sign(nativePayload, nativeSecret, nativeOptions)
: jwt.sign(nativePayload, nativeSecret);
return marshallToVm(token, vm);
} catch (err) {
throw vm.newError(err.message || String(err));
}
});

vm.setProp(vm.global, '__bruno__jwt__sign', _jwtSign);
_jwtSign.dispose();

// --- verify ---
const _jwtVerify = vm.newFunction('verify', function (token, secret, options, callback) {
const nativeToken = vm.dump(token);
const nativeSecret = vm.dump(secret);

let nativeOptions;
let actualCallback = callback;

const optionsType = options === undefined ? 'undefined' : vm.typeof(options);
if (optionsType === 'function') {
actualCallback = options;
nativeOptions = undefined;
} else if (optionsType === 'object' && options !== null) {
nativeOptions = vm.dump(options);
}

if (actualCallback && vm.typeof(actualCallback) === 'function') {

let decodedResult;
let hostError;
try {
decodedResult = nativeOptions
? jwt.verify(nativeToken, nativeSecret, nativeOptions)
: jwt.verify(nativeToken, nativeSecret);
} catch (err) {
hostError = err;
}

try {
if (hostError) {
const vmErr = vm.newError(hostError.message || String(hostError));
invokeFunction(vm, actualCallback, [vmErr, vm.undefined])
.catch((e) => {
console.warn('[JWT SHIM][verify.cb] callback invocation error:', e);
})
.finally(() => {
vmErr.dispose();
actualCallback.dispose();
});
} else {
const vmNull = vm.null;
const vmDecoded = marshallToVm(decodedResult, vm);
invokeFunction(vm, actualCallback, [vmNull, vmDecoded])
.catch((e) => {
console.warn('[JWT SHIM][verify.cb] callback invocation error:', e);
})
.finally(() => {
vmDecoded.dispose();
actualCallback.dispose();
});
}
} catch (e) {
console.warn('[JWT SHIM][verify.cb] unexpected error:', e);
actualCallback.dispose();
}

return vm.undefined;
}

try {
const decoded = nativeOptions
? jwt.verify(nativeToken, nativeSecret, nativeOptions)
: jwt.verify(nativeToken, nativeSecret);
return marshallToVm(decoded, vm);
} catch (err) {
throw vm.newError(err.message || String(err));
}
});

vm.setProp(vm.global, '__bruno__jwt__verify', _jwtVerify);
_jwtVerify.dispose();

// --- decode ---
const _jwtDecode = vm.newFunction('decode', function (token, options) {
const nativeToken = vm.dump(token);

let nativeOptions;
const optionsType = options === undefined ? 'undefined' : vm.typeof(options);
if (optionsType === 'object' && options !== null) {
nativeOptions = vm.dump(options);
}

try {
const decoded = nativeOptions
? jwt.decode(nativeToken, nativeOptions)
: jwt.decode(nativeToken);
return marshallToVm(decoded, vm);
} catch (err) {
throw vm.newError(err.message || String(err));
}
});

vm.setProp(vm.global, '__bruno__jwt__decode', _jwtDecode);
_jwtDecode.dispose();

vm.evalCode(`
globalThis.jwt = {};
globalThis.jwt.sign = globalThis.__bruno__jwt__sign;
globalThis.jwt.verify = globalThis.__bruno__jwt__verify;
globalThis.jwt.decode = globalThis.__bruno__jwt__decode;
globalThis.requireObject = {
...globalThis.requireObject,
'jsonwebtoken': globalThis.jwt,
};
`);
};

module.exports = addJwtShimToContext;
52 changes: 51 additions & 1 deletion packages/bruno-js/src/sandbox/quickjs/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,56 @@ const marshallToVm = (value, vm) => {
}
};

/**
* Invokes a QuickJS function handle.
* - Returns a Promise
*
* @param {Object} vm - QuickJS VM instance
* @param {QuickJSHandle} quickFn - A QuickJS function handle
* @param {Array} args - Arguments to pass to the function
* @returns {Promise<any>} - The result as a Promise
*/
async function invokeFunction(vm, quickFn, args = []) {
if (vm.typeof(quickFn) !== 'function') {
throw new TypeError('Target is not a QuickJS function');
}

const result = vm.callFunction(quickFn, vm.global, ...args);

if (result.error) {
const error = vm.dump(result.error);
result.error.dispose();
throw error;
}

// Check if the result is a QuickJS Promise handle (async functions)
if (vm.typeof(result.value) === 'object' && result.value.constructor && vm.typeof(result.value.constructor) === 'function') {
try {
const promiseHandle = vm.unwrapResult(result);
const resolvedResult = await vm.resolvePromise(promiseHandle);
promiseHandle.dispose();
const resolvedHandle = vm.unwrapResult(resolvedResult);
const value = vm.dump(resolvedHandle);
resolvedHandle.dispose();
return Promise.resolve(value);
} catch (promiseError) {
// If it's not a valid Promise, throw an error
result.value.dispose();
throw new Error(`Invalid Promise handle: ${promiseError.message}`);
}
}

const value = vm.dump(result.value);
result.value.dispose();

return (value && typeof value.then === 'function')
? value
: Promise.resolve(value);
}



module.exports = {
marshallToVm
marshallToVm,
invokeFunction
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"version": "1",
"name": "jsonwebtoken",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
meta {
name: decode
type: http
seq: 1
}

post {
url: {{host}}/api/echo
body: none
auth: inherit
}

script:pre-request {
const jwt = require('jsonwebtoken');

const testPayload = {
userId: 456,
username: 'decodeuser',
role: 'user',
iat: Math.floor(Date.now() / 1000)
};

const secret = bru.getEnvVar('secret') || 'test-secret-key';
const testToken = jwt.sign(testPayload, secret, { algorithm: 'HS256', expiresIn: '1h' });

try {
console.log('Testing JWT decoding...');
console.log('Test token:', testToken);

const decoded = jwt.decode(testToken);

bru.setEnvVar('decoded_payload', JSON.stringify(decoded));

} catch (error) {
console.error('JWT decoding failed:', error.message);
throw error;
}
}

tests {
test("Decoded payload should exist", function() {
const decodedPayload = bru.getEnvVar('decoded_payload');
expect(decodedPayload).to.exist;
});

test("Decoded payload should contain correct user data", function() {
const decodedPayload = JSON.parse(bru.getEnvVar('decoded_payload'));

expect(decodedPayload.userId).to.equal(456);
expect(decodedPayload.username).to.equal('decodeuser');
expect(decodedPayload.role).to.equal('user');
});

test("Decoded payload should have timestamp fields", function() {
const decodedPayload = JSON.parse(bru.getEnvVar('decoded_payload'));

expect(decodedPayload.iat).to.exist;
expect(decodedPayload.exp).to.exist;
expect(typeof decodedPayload.iat).to.equal('number');
expect(typeof decodedPayload.exp).to.equal('number');
});
}

settings {
encodeUrl: true
}
Loading
Loading