diff --git a/packages/bruno-app/src/providers/App/useIpcEvents.js b/packages/bruno-app/src/providers/App/useIpcEvents.js index a890f1979..84b95e1a6 100644 --- a/packages/bruno-app/src/providers/App/useIpcEvents.js +++ b/packages/bruno-app/src/providers/App/useIpcEvents.js @@ -35,7 +35,7 @@ import toast from 'react-hot-toast'; import { useDispatch, useStore } from 'react-redux'; import { isElectron } from 'utils/common/platform'; import { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments'; -import { collectionAddOauth2CredentialsByUrl, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index'; +import { collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByCredentialsId, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index'; import { addLog } from 'providers/ReduxStore/slices/logs'; import { updateSystemResources } from 'providers/ReduxStore/slices/performance'; import { apiSpecAddFileEvent, apiSpecChangeFileEvent } from 'providers/ReduxStore/slices/apiSpec'; @@ -318,6 +318,10 @@ const useIpcEvents = () => { dispatch(collectionAddOauth2CredentialsByUrl(payload)); }); + const removeCollectionOauth2CredentialsClearListener = ipcRenderer.on('main:credentials-clear', (val) => { + dispatch(collectionClearOauth2CredentialsByCredentialsId(val)); + }); + const removeHttpStreamNewDataListener = ipcRenderer.on('main:http-stream-new-data', (val) => { dispatch(streamDataReceived(val)); }); @@ -360,6 +364,7 @@ const useIpcEvents = () => { removeGlobalEnvironmentsUpdatesListener(); removeSnapshotHydrationListener(); removeCollectionOauth2CredentialsUpdatesListener(); + removeCollectionOauth2CredentialsClearListener(); removeHttpStreamNewDataListener(); removeHttpStreamEndListener(); removeCollectionLoadingStateListener(); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index 2a048b008..8da7837e8 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -44,7 +44,7 @@ import { updateLastAction, setCollectionSecurityConfig, collectionAddOauth2CredentialsByUrl, - collectionClearOauth2CredentialsByUrl, + collectionClearOauth2CredentialsByUrlAndCredentialsId, initRunRequestEvent, updateRunnerConfiguration as _updateRunnerConfiguration, updateActiveConnections, @@ -2851,9 +2851,10 @@ export const clearOauth2Cache = (payload) => async (dispatch, getState) => { .invoke('clear-oauth2-cache', collectionUid, url, credentialsId) .then(() => { dispatch( - collectionClearOauth2CredentialsByUrl({ + collectionClearOauth2CredentialsByUrlAndCredentialsId({ url, - collectionUid + collectionUid, + credentialsId }) ); resolve(); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index ae68f9ccb..dc7220c74 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -3217,7 +3217,8 @@ export const collectionsSlice = createSlice({ } }, - collectionClearOauth2CredentialsByUrl: (state, action) => { + // Clears a specific credential matching url + collectionUid + credentialsId (used by UI "Clear OAuth2 Cache") + collectionClearOauth2CredentialsByUrlAndCredentialsId: (state, action) => { const { collectionUid, url, credentialsId } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); if (!collection) return; @@ -3227,21 +3228,23 @@ export const collectionsSlice = createSlice({ const filteredOauth2Credentials = filter( collectionOauth2Credentials, (creds) => - !(creds.url === url && creds.collectionUid === collectionUid) + !(creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId) ); collection.oauth2Credentials = filteredOauth2Credentials; } }, - collectionGetOauth2CredentialsByUrl: (state, action) => { - const { collectionUid, url, credentialsId } = action.payload; + // Clears all credentials matching credentialsId regardless of URL (used by script bru.resetOauth2Credential) + collectionClearOauth2CredentialsByCredentialsId: (state, action) => { + const { collectionUid, credentialsId } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); - const oauth2Credential = find( - collection?.oauth2Credentials || [], - (creds) => - creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId - ); - return oauth2Credential; + if (!collection) return; + + if (collection.oauth2Credentials) { + collection.oauth2Credentials = collection.oauth2Credentials.filter( + (creds) => creds.credentialsId !== credentialsId + ); + } }, updateFolderAuthMode: (state, action) => { @@ -3676,8 +3679,8 @@ export const { moveCollection, streamDataReceived, collectionAddOauth2CredentialsByUrl, - collectionClearOauth2CredentialsByUrl, - collectionGetOauth2CredentialsByUrl, + collectionClearOauth2CredentialsByUrlAndCredentialsId, + collectionClearOauth2CredentialsByCredentialsId, updateFolderAuth, updateFolderAuthMode, addRequestTag, diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js index ed21e0f76..041624ae5 100644 --- a/packages/bruno-app/src/utils/codemirror/autocomplete.js +++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js @@ -116,7 +116,8 @@ const STATIC_API_HINTS = { 'bru.cookies.jar().deleteCookie(url, name, callback)', 'bru.utils', 'bru.utils.minifyJson(json)', - 'bru.utils.minifyXml(xml)' + 'bru.utils.minifyXml(xml)', + 'bru.resetOauth2Credential(credentialId)' ] }; diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 26bccc8b6..ee46286f9 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -23,7 +23,8 @@ const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const { NtlmClient } = require('axios-ntlm'); const { addDigestInterceptor, getHttpHttpsAgents, makeAxiosInstance: makeAxiosInstanceForOauth2 } = require('@usebruno/requests'); const { getCACertificates, transformProxyConfig } = require('@usebruno/requests'); -const { getOAuth2Token } = require('../utils/oauth2'); +const { getOAuth2Token, getFormattedOauth2Credentials } = require('../utils/oauth2'); +const tokenStore = require('../store/tokenStore'); const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData } = require('@usebruno/common').utils; const onConsoleLog = (type, args) => { @@ -225,6 +226,12 @@ const runSingleRequest = async function ( shouldStopRunnerExecution = true; } + if (result?.oauth2CredentialsToReset?.length) { + for (const credentialId of result.oauth2CredentialsToReset) { + tokenStore.deleteCredentialById(credentialId); + } + } + if (result?.skipRequest) { return { test: { @@ -633,6 +640,8 @@ const runSingleRequest = async function ( console.error('OAuth2 token fetch error:', error.message); } + request.oauth2CredentialVariables = getFormattedOauth2Credentials(); + // Remove oauth2 config from request to prevent it from being sent delete request.oauth2; } @@ -787,6 +796,12 @@ const runSingleRequest = async function ( shouldStopRunnerExecution = true; } + if (result?.oauth2CredentialsToReset?.length) { + for (const credentialId of result.oauth2CredentialsToReset) { + tokenStore.deleteCredentialById(credentialId); + } + } + postResponseTestResults = result?.results || []; logResults(postResponseTestResults, 'Post-Response Tests'); } catch (error) { @@ -858,6 +873,12 @@ const runSingleRequest = async function ( shouldStopRunnerExecution = true; } + if (result?.oauth2CredentialsToReset?.length) { + for (const credentialId of result.oauth2CredentialsToReset) { + tokenStore.deleteCredentialById(credentialId); + } + } + logResults(testResults, 'Tests'); } catch (error) { console.error('Test script execution error:', error); diff --git a/packages/bruno-cli/src/store/tokenStore.js b/packages/bruno-cli/src/store/tokenStore.js index 9dedcfc54..f53fd636a 100644 --- a/packages/bruno-cli/src/store/tokenStore.js +++ b/packages/bruno-cli/src/store/tokenStore.js @@ -29,6 +29,15 @@ const tokenStore = { return false; }, + // Delete all credentials for a given credentialsId (all URLs) + deleteCredentialById(credentialsId) { + if (this.credentials[credentialsId]) { + delete this.credentials[credentialsId]; + return true; + } + return false; + }, + // Get all stored OAuth2 credentials getAllCredentials() { const result = []; diff --git a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js index c5580edaf..f713e828d 100644 --- a/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js +++ b/packages/bruno-electron/src/ipc/network/authorize-user-in-window.js @@ -128,15 +128,31 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session, additionalH function onWindowRedirect(url) { // Handle redirects as needed + let urlObj; + let callbackUrlObj; + + try { + urlObj = new URL(url); + } catch (e) { + // Invalid redirect URL, skip processing + return; + } + + try { + callbackUrlObj = new URL(callbackUrl); + } catch (e) { + // Invalid callback URL, skip matching but still check for errors below + callbackUrlObj = null; + } // Check if redirect is to the callback URL and contains an authorization code - if (matchesCallbackUrl(new URL(url), new URL(callbackUrl))) { + if (callbackUrlObj && matchesCallbackUrl(urlObj, callbackUrlObj)) { finalUrl = url; window.close(); + return; } // Handle OAuth error responses - const urlObj = new URL(url); if (urlObj.searchParams.has('error')) { const error = urlObj.searchParams.get('error'); const errorDescription = urlObj.searchParams.get('error_description'); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 70ac1d89c..39b43f358 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -25,7 +25,7 @@ const { chooseFileToSave, writeFile, getCollectionFormat, hasRequestExtension } const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const { createFormData } = require('../../utils/form-data'); const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence } = require('../../utils/collection'); -const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, updateCollectionOauth2Credentials } = require('../../utils/oauth2'); +const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, updateCollectionOauth2Credentials, clearOauth2CredentialsByCredentialsId } = require('../../utils/oauth2'); const { preferencesUtil } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); const { getBrunoConfig } = require('../../store/bruno-config'); @@ -477,6 +477,25 @@ const registerNetworkIpc = (mainWindow) => { }; }; + const resetOauth2Credentials = ({ oauth2CredentialsToReset, request, collectionUid }) => { + if (!oauth2CredentialsToReset?.length) return; + for (const credentialId of oauth2CredentialsToReset) { + clearOauth2CredentialsByCredentialsId({ collectionUid, credentialsId: credentialId }); + if (request?.oauth2Credentials?.credentialsId === credentialId) { + request.oauth2Credentials = null; + } + const prefix = `$oauth2.${credentialId}.`; + if (request.oauth2CredentialVariables) { + for (const key of Object.keys(request.oauth2CredentialVariables)) { + if (key.startsWith(prefix)) { + delete request.oauth2CredentialVariables[key]; + } + } + } + mainWindow.webContents.send('main:credentials-clear', { collectionUid, credentialsId: credentialId }); + } + }; + const runPreRequest = async ( request, requestUid, @@ -528,6 +547,8 @@ const registerNetworkIpc = (mainWindow) => { collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables; + resetOauth2Credentials({ oauth2CredentialsToReset: scriptResult.oauth2CredentialsToReset, request, collectionUid }); + const domainsWithCookies = await getDomainsWithCookies(); mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies))); } @@ -666,6 +687,8 @@ const registerNetworkIpc = (mainWindow) => { collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables; + resetOauth2Credentials({ oauth2CredentialsToReset: scriptResult.oauth2CredentialsToReset, request, collectionUid }); + const domainsWithCookiesPost = await getDomainsWithCookies(); mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPost))); } @@ -816,7 +839,7 @@ const registerNetworkIpc = (mainWindow) => { cancelTokenUid }); - if (request?.oauth2Credentials) { + if (request.oauth2Credentials?.credentials && request.oauth2Credentials?.credentialsId) { mainWindow.webContents.send('main:credentials-update', { credentials: request?.oauth2Credentials?.credentials, url: request?.oauth2Credentials?.url, @@ -825,6 +848,12 @@ const registerNetworkIpc = (mainWindow) => { ...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }), debugInfo: request?.oauth2Credentials?.debugInfo }); + + const { credentialsId, credentials } = request.oauth2Credentials; + request.oauth2CredentialVariables = request.oauth2CredentialVariables || {}; + Object.entries(credentials).forEach(([key, value]) => { + request.oauth2CredentialVariables[`$oauth2.${credentialsId}.${key}`] = value; + }); } let response, responseTime, axiosDataStream; @@ -1032,6 +1061,8 @@ const registerNetworkIpc = (mainWindow) => { collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; + resetOauth2Credentials({ oauth2CredentialsToReset: testResults.oauth2CredentialsToReset, request, collectionUid }); + !runInBackground && notifyScriptExecution({ channel: 'main:run-request-event', basePayload: { requestUid, collectionUid, itemUid: item.uid }, @@ -1487,7 +1518,7 @@ const registerNetworkIpc = (mainWindow) => { collection.globalEnvironmentVariables ); - if (request?.oauth2Credentials) { + if (request.oauth2Credentials?.credentials && request.oauth2Credentials?.credentialsId) { mainWindow.webContents.send('main:credentials-update', { credentials: request?.oauth2Credentials?.credentials, url: request?.oauth2Credentials?.url, @@ -1497,6 +1528,12 @@ const registerNetworkIpc = (mainWindow) => { debugInfo: request?.oauth2Credentials?.debugInfo }); + const { credentialsId, credentials } = request.oauth2Credentials; + request.oauth2CredentialVariables = request.oauth2CredentialVariables || {}; + Object.entries(credentials).forEach(([key, value]) => { + request.oauth2CredentialVariables[`$oauth2.${credentialsId}.${key}`] = value; + }); + collection.oauth2Credentials = updateCollectionOauth2Credentials({ itemUid: item.uid, collectionUid, @@ -1738,6 +1775,8 @@ const registerNetworkIpc = (mainWindow) => { collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; + resetOauth2Credentials({ oauth2CredentialsToReset: testResults.oauth2CredentialsToReset, request, collectionUid }); + notifyScriptExecution({ channel: 'main:run-folder-event', basePayload: eventData, diff --git a/packages/bruno-electron/src/store/oauth2.js b/packages/bruno-electron/src/store/oauth2.js index 41522e37d..bebaf9e9c 100644 --- a/packages/bruno-electron/src/store/oauth2.js +++ b/packages/bruno-electron/src/store/oauth2.js @@ -150,6 +150,23 @@ class Oauth2Store { } } + clearCredentialsByCredentialsId({ collectionUid, credentialsId }) { + try { + let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid }); + let filteredCredentials = oauth2DataForCollection?.credentials?.filter( + (c) => c?.credentialsId !== credentialsId + ); + let newOauth2DataForCollection = { + ...oauth2DataForCollection, + credentials: filteredCredentials + }; + this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection }); + return newOauth2DataForCollection; + } catch (err) { + console.log('error clearing oauth2 credentials by credentialsId from cache', err); + } + } + clearCredentialsForCollection({ collectionUid, url, credentialsId }) { try { let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url }); diff --git a/packages/bruno-electron/src/utils/oauth2.js b/packages/bruno-electron/src/utils/oauth2.js index 0a6227df0..6af08a41b 100644 --- a/packages/bruno-electron/src/utils/oauth2.js +++ b/packages/bruno-electron/src/utils/oauth2.js @@ -25,6 +25,10 @@ const clearOauth2Credentials = ({ collectionUid, url, credentialsId }) => { oauth2Store.clearCredentialsForCollection({ collectionUid, url, credentialsId }); }; +const clearOauth2CredentialsByCredentialsId = ({ collectionUid, credentialsId }) => { + oauth2Store.clearCredentialsByCredentialsId({ collectionUid, credentialsId }); +}; + const getStoredOauth2Credentials = ({ collectionUid, url, credentialsId }) => { try { const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId }); @@ -941,6 +945,7 @@ const updateCollectionOauth2Credentials = ({ collectionUid, itemUid, collectionO module.exports = { persistOauth2Credentials, clearOauth2Credentials, + clearOauth2CredentialsByCredentialsId, getStoredOauth2Credentials, getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, diff --git a/packages/bruno-js/src/bru.js b/packages/bruno-js/src/bru.js index 5e500cf15..a66f9e2ed 100644 --- a/packages/bruno-js/src/bru.js +++ b/packages/bruno-js/src/bru.js @@ -92,6 +92,8 @@ class Bru { }; // Holds variables that are marked as persistent by scripts this.persistentEnvVariables = {}; + // Holds credential IDs to be reset after script execution + this.oauth2CredentialsToReset = []; this.runner = { skipRequest: () => { this.skipRequest = true; @@ -275,6 +277,24 @@ class Bru { return this.interpolate(this.oauth2CredentialVariables[key]); } + resetOauth2Credential(credentialId) { + if (!credentialId || typeof credentialId !== 'string') { + throw new Error('credentialId must be a non-empty string'); + } + + if (!this.oauth2CredentialsToReset.includes(credentialId)) { + this.oauth2CredentialsToReset.push(credentialId); + } + + // Remove matching credential variables so subsequent getOauth2CredentialVar() calls return undefined + const prefix = `$oauth2.${credentialId}.`; + for (const key of Object.keys(this.oauth2CredentialVariables)) { + if (key.startsWith(prefix)) { + delete this.oauth2CredentialVariables[key]; + } + } + } + hasVar(key) { return Object.hasOwn(this.runtimeVariables, key); } diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 28cd48e75..7032eea73 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -76,6 +76,7 @@ class ScriptRuntime { runtimeVariables: cleanJson(runtimeVariables), persistentEnvVariables: bru.persistentEnvVariables, globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + oauth2CredentialsToReset: bru.oauth2CredentialsToReset, results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, @@ -193,6 +194,7 @@ class ScriptRuntime { persistentEnvVariables: cleanJson(bru.persistentEnvVariables), runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), + oauth2CredentialsToReset: bru.oauth2CredentialsToReset, results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest, skipRequest: bru.skipRequest, diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index 88f30a95c..894a028e1 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -27,13 +27,14 @@ class TestRuntime { collectionName ) { const globalEnvironmentVariables = request?.globalEnvironmentVariables || {}; + const oauth2CredentialVariables = request?.oauth2CredentialVariables || {}; const collectionVariables = request?.collectionVariables || {}; const folderVariables = request?.folderVariables || {}; const requestVariables = request?.requestVariables || {}; const promptVariables = request?.promptVariables || {}; const assertionResults = request?.assertionResults || []; const certsAndProxyConfig = request?.certsAndProxyConfig; - const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, {}, collectionName, promptVariables, certsAndProxyConfig); + const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig); const req = new BrunoRequest(request); const res = new BrunoResponse(response); @@ -109,6 +110,7 @@ class TestRuntime { runtimeVariables: cleanJson(runtimeVariables), globalEnvironmentVariables: cleanJson(globalEnvironmentVariables), persistentEnvVariables: cleanJson(bru.persistentEnvVariables), + oauth2CredentialsToReset: bru.oauth2CredentialsToReset, results: cleanJson(__brunoTestResults.getResults()), nextRequestName: bru.nextRequest }; diff --git a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js index 8f4044ef3..69db5cd2c 100644 --- a/packages/bruno-js/src/sandbox/quickjs/shims/bru.js +++ b/packages/bruno-js/src/sandbox/quickjs/shims/bru.js @@ -89,6 +89,12 @@ const addBruShimToContext = (vm, bru) => { vm.setProp(bruObject, 'getOauth2CredentialVar', getOauth2CredentialVar); getOauth2CredentialVar.dispose(); + let resetOauth2Credential = vm.newFunction('resetOauth2Credential', function (credentialId) { + bru.resetOauth2Credential(vm.dump(credentialId)); + }); + vm.setProp(bruObject, 'resetOauth2Credential', resetOauth2Credential); + resetOauth2Credential.dispose(); + let setGlobalEnvVar = vm.newFunction('setGlobalEnvVar', function (key, value) { bru.setGlobalEnvVar(vm.dump(key), vm.dump(value)); });