CVE-2024-34347
Description
@hoppscotch/cli is a CLI to run Hoppscotch Test Scripts in CI environments. Prior to 0.8.0, the @hoppscotch/js-sandbox package provides a Javascript sandbox that uses the Node.js vm module. However, the vm module is not safe for sandboxing untrusted Javascript code. This is because code inside the vm context can break out if it can get a hold of any reference to an object created outside of the vm. In the case of @hoppscotch/js-sandbox, multiple references to external objects are passed into the vm context to allow pre-request scripts interactions with environment variables and more. But this also allows the pre-request script to escape the sandbox. This vulnerability is fixed in 0.8.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@hoppscotch/clinpm | >= 0.5.0, < 0.8.0 | 0.8.0 |
Patches
122c6eabd1331chore: migrate `Node.js` implementation for `js-sandbox` to `isolated-vm` (#3973)
52 files changed · +1029 −286
.github/workflows/tests.yml+7 −8 modified@@ -17,22 +17,21 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup environment run: mv .env.example .env + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Setup pnpm - uses: pnpm/action-setup@v2.2.4 + uses: pnpm/action-setup@v3 with: version: 8 run_install: true - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node }} - cache: pnpm - - name: Run tests run: pnpm test
packages/hoppscotch-cli/bin/hopp.js+26 −1 modified@@ -1,6 +1,31 @@ #!/usr/bin/env node // * The entry point of the CLI +// @ts-check import { cli } from "../dist/index.js"; -cli(process.argv); +import { spawnSync } from "child_process"; +import { cloneDeep } from "lodash-es"; + +const nodeVersion = parseInt(process.versions.node.split(".")[0]); + +// As per isolated-vm documentation, we need to supply `--no-node-snapshot` for node >= 20 +// src: https://github.com/laverdet/isolated-vm?tab=readme-ov-file#requirements +if (nodeVersion >= 20 && !process.execArgv.includes("--no-node-snapshot")) { + const argCopy = cloneDeep(process.argv); + + // Replace first argument with --no-node-snapshot + // We can get argv[0] from process.argv0 + argCopy[0] = "--no-node-snapshot"; + + const result = spawnSync( + process.argv0, + argCopy, + { stdio: "inherit" } + ); + + // Exit with the same status code as the spawned process + process.exit(result.status ?? 0); +} else { + cli(process.argv); +}
packages/hoppscotch-cli/package.json+2 −1 modified@@ -1,6 +1,6 @@ { "name": "@hoppscotch/cli", - "version": "0.7.0", + "version": "0.7.1", "description": "A CLI to run Hoppscotch test scripts in CI environments.", "homepage": "https://hoppscotch.io", "type": "module", @@ -44,6 +44,7 @@ "axios": "1.6.7", "chalk": "5.3.0", "commander": "11.1.0", + "isolated-vm": "4.7.2", "lodash-es": "4.17.21", "qs": "6.11.2", "verzod": "0.2.2",
packages/hoppscotch-cli/src/index.ts+2 −1 modified@@ -1,6 +1,7 @@ import chalk from "chalk"; import { Command } from "commander"; import * as E from "fp-ts/Either"; + import { version } from "../package.json"; import { test } from "./commands/test"; import { handleError } from "./handlers/error"; @@ -20,7 +21,7 @@ const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent( "https://docs.hoppscotch.io/documentation/clients/cli" )}`; -const program = new Command() +const program = new Command(); program .name("hopp")
packages/hoppscotch-cli/src/__tests__/commands/test.spec.ts+1 −1 modified@@ -224,7 +224,7 @@ describe("Test `hopp test <file> --env <file>` command:", () => { }); describe("Secret environment variables", () => { - jest.setTimeout(10000); + jest.setTimeout(100000); // Reads secret environment values from system environment test("Successfully picks the values for secret environment variables from `process.env` and persists the variables set from the pre-request script", async () => {
packages/hoppscotch-cli/src/__tests__/samples/collections/coll-v1-req-v0.json+53 −25 modified@@ -1,27 +1,55 @@ { - "v": 1, - "name": "coll-v1", - "folders": [], - "requests": [ + "v": 1, + "name": "coll-v1", + "folders": [ + { + "v": 1, + "name": "coll-v1-child", + "folders": [], + "requests": [ { - "url": "https://httpbin.org", - "path": "/get", - "headers": [ - { "key": "Inactive-Header", "value": "Inactive Header", "active": false }, - { "key": "Authorization", "value": "Bearer token123", "active": true } - ], - "params": [ - { "key": "key", "value": "value", "active": true }, - { "key": "inactive-key", "value": "inactive-param", "active": false } - ], - "name": "req-v0", - "method": "GET", - "preRequestScript": "", - "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})", - "contentType": "application/json", - "body": "", - "auth": "Bearer Token", - "bearerToken": "token123" - } - ] -} \ No newline at end of file + "url": "https://echo.hoppscotch.io", + "path": "/get", + "headers": [ + { "key": "Inactive-Header", "value": "Inactive Header", "active": false }, + { "key": "Authorization", "value": "Bearer token123", "active": true } + ], + "params": [ + { "key": "key", "value": "value", "active": true }, + { "key": "inactive-key", "value": "inactive-param", "active": false } + ], + "name": "req-v0-II", + "method": "GET", + "preRequestScript": "", + "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})", + "contentType": "application/json", + "body": "", + "auth": "Bearer Token", + "bearerToken": "token123" + } + ] + } + ], + "requests": [ + { + "url": "https://echo.hoppscotch.io", + "path": "/get", + "headers": [ + { "key": "Inactive-Header", "value": "Inactive Header", "active": false }, + { "key": "Authorization", "value": "Bearer token123", "active": true } + ], + "params": [ + { "key": "key", "value": "value", "active": true }, + { "key": "inactive-key", "value": "inactive-param", "active": false } + ], + "name": "req-v0", + "method": "GET", + "preRequestScript": "", + "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})", + "contentType": "application/json", + "body": "", + "auth": "Bearer Token", + "bearerToken": "token123" + } + ] +}
packages/hoppscotch-cli/src/__tests__/samples/collections/coll-v1-req-v1.json+52 −3 modified@@ -1,11 +1,60 @@ { "v": 1, "name": "coll-v1", - "folders": [], + "folders": [ + { + "v": 1, + "name": "coll-v1-child", + "folders": [], + "requests": [ + { + "v": "1", + "endpoint": "https://echo.hoppscotch.io", + "headers": [ + { + "key": "Inactive-Header", + "value": "Inactive Header", + "active": false + }, + { + "key": "Authorization", + "value": "Bearer token123", + "active": true + } + ], + "params": [ + { + "key": "key", + "value": "value", + "active": true + }, + { + "key": "inactive-key", + "value": "inactive-param", + "active": false + } + ], + "name": "req-v1-II", + "method": "GET", + "preRequestScript": "", + "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})", + "body": { + "contentType": null, + "body": null + }, + "auth": { + "authType": "bearer", + "authActive": true, + "token": "token123" + } + } + ] + } + ], "requests": [ { "v": "1", - "endpoint": "https://httpbin.org/get", + "endpoint": "https://echo.hoppscotch.io", "headers": [ { "key": "Inactive-Header", @@ -33,7 +82,7 @@ "name": "req-v1", "method": "GET", "preRequestScript": "", - "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})", + "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})", "body": { "contentType": null, "body": null
packages/hoppscotch-cli/src/__tests__/samples/collections/coll-v2-req-v2.json+58 −3 modified@@ -1,11 +1,66 @@ { "v": 2, "name": "coll-v2", - "folders": [], + "folders": [ + { + "v": 2, + "name": "coll-v2-child", + "folders": [], + "requests": [ + { + "v": "2", + "endpoint": "https://echo.hoppscotch.io", + "headers": [ + { + "key": "Inactive-Header", + "value": "Inactive Header", + "active": false + }, + { + "key": "Authorization", + "value": "Bearer token123", + "active": true + } + ], + "params": [ + { + "key": "key", + "value": "value", + "active": true + }, + { + "key": "inactive-key", + "value": "inactive-param", + "active": false + } + ], + "name": "req-v2-II", + "method": "GET", + "preRequestScript": "", + "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})", + "body": { + "contentType": null, + "body": null + }, + "auth": { + "authType": "bearer", + "authActive": true, + "token": "token123" + }, + "requestVariables": [] + } + ], + "auth": { + "authType": "inherit", + "authActive": true + }, + "headers": [] + } + ], "requests": [ { "v": "2", - "endpoint": "https://httpbin.org/get", + "endpoint": "https://echo.hoppscotch.io", "headers": [ { "key": "Inactive-Header", @@ -33,7 +88,7 @@ "name": "req-v2", "method": "GET", "preRequestScript": "", - "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})", + "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})", "body": { "contentType": null, "body": null
packages/hoppscotch-cli/src/__tests__/samples/collections/coll-v2-req-v3.json+58 −3 modified@@ -1,11 +1,66 @@ { "v": 2, "name": "coll-v2", - "folders": [], + "folders": [ + { + "v": 2, + "name": "coll-v2-child", + "folders": [], + "requests": [ + { + "v": "3", + "endpoint": "https://echo.hoppscotch.io", + "headers": [ + { + "key": "Inactive-Header", + "value": "Inactive Header", + "active": false + }, + { + "key": "Authorization", + "value": "Bearer token123", + "active": true + } + ], + "params": [ + { + "key": "key", + "value": "value", + "active": true + }, + { + "key": "inactive-key", + "value": "inactive-param", + "active": false + } + ], + "name": "req-v3-II", + "method": "GET", + "preRequestScript": "", + "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})", + "body": { + "contentType": null, + "body": null + }, + "auth": { + "authType": "bearer", + "authActive": true, + "token": "token123" + }, + "requestVariables": [] + } + ], + "auth": { + "authType": "inherit", + "authActive": true + }, + "headers": [] + } + ], "requests": [ { "v": "3", - "endpoint": "https://httpbin.org/get", + "endpoint": "https://echo.hoppscotch.io", "headers": [ { "key": "Inactive-Header", @@ -33,7 +88,7 @@ "name": "req-v3", "method": "GET", "preRequestScript": "", - "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})", + "testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"inactive-header\"]).toBe(undefined)\n})", "body": { "contentType": null, "body": null
packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-coll.json+51 −21 modified@@ -5,8 +5,14 @@ "requests": [ { "v": "3", - "auth": { "authType": "none", "authActive": true }, - "body": { "body": null, "contentType": null }, + "auth": { + "authType": "none", + "authActive": true + }, + "body": { + "body": null, + "contentType": null + }, "name": "test-secret-headers", "method": "GET", "params": [], @@ -18,13 +24,16 @@ } ], "requestVariables": [], - "endpoint": "<<baseURL>>/headers", - "testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})", + "endpoint": "<<echoHoppBaseURL>>/headers", + "testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"secret-header-key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})", "preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)" }, { "v": "3", - "auth": { "authType": "none", "authActive": true }, + "auth": { + "authType": "none", + "authActive": true + }, "body": { "body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}", "contentType": "application/json" @@ -34,14 +43,20 @@ "params": [], "headers": [], "requestVariables": [], - "endpoint": "<<baseURL>>/post", - "testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})", + "endpoint": "<<echoHoppBaseURL>>/post", + "testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(JSON.parse(pw.response.body.data).secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})", "preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)" }, { "v": "3", - "auth": { "authType": "none", "authActive": true }, - "body": { "body": null, "contentType": null }, + "auth": { + "authType": "none", + "authActive": true + }, + "body": { + "body": null, + "contentType": null + }, "name": "test-secret-query-params", "method": "GET", "params": [ @@ -53,7 +68,7 @@ ], "headers": [], "requestVariables": [], - "endpoint": "<<baseURL>>/get", + "endpoint": "<<echoHoppBaseURL>>", "testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})", "preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)" }, @@ -65,14 +80,17 @@ "username": "<<secretBasicAuthUsername>>", "authActive": true }, - "body": { "body": null, "contentType": null }, + "body": { + "body": null, + "contentType": null + }, "name": "test-secret-basic-auth", "method": "GET", "params": [], "headers": [], "requestVariables": [], - "endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>", - "testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});", + "endpoint": "<<httpbinBaseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>", + "testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n // The endpoint at times results in a `502` bad gateway\n if (pw.response.status !== 200) {\n return\n }\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});", "preRequestScript": "" }, { @@ -84,30 +102,42 @@ "username": "testuser", "authActive": true }, - "body": { "body": null, "contentType": null }, + "body": { + "body": null, + "contentType": null + }, "name": "test-secret-bearer-auth", "method": "GET", "params": [], "headers": [], "requestVariables": [], - "endpoint": "<<baseURL>>/bearer", - "testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});", + "endpoint": "<<httpbinBaseURL>>/bearer", + "testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n // Safeguard to prevent test failures due to the endpoint\n if (pw.response.status !== 200) {\n return\n }\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});", "preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)" }, { "v": "3", - "auth": { "authType": "none", "authActive": true }, - "body": { "body": null, "contentType": null }, + "auth": { + "authType": "none", + "authActive": true + }, + "body": { + "body": null, + "contentType": null + }, "name": "test-secret-fallback", "method": "GET", "params": [], "headers": [], "requestVariables": [], - "endpoint": "<<baseURL>>", + "endpoint": "<<echoHoppBaseURL>>", "testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})", "preRequestScript": "" } ], - "auth": { "authType": "inherit", "authActive": false }, + "auth": { + "authType": "inherit", + "authActive": false + }, "headers": [] -} +} \ No newline at end of file
packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-coll.json+13 −13 modified@@ -1,6 +1,6 @@ { "v": 2, - "name": "secret-envs-setters-coll", + "name": "secret-envs-persistence-coll", "folders": [], "requests": [ { @@ -24,8 +24,8 @@ "active": true } ], - "endpoint": "<<baseURL>>/headers", - "testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})", + "endpoint": "<<echoHoppBaseURL>>", + "testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"secret-header-key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})", "preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)" }, { @@ -49,8 +49,8 @@ "active": true } ], - "endpoint": "<<baseURL>>/headers", - "testScript": "pw.test(\"Value set at the pre-request script takes precedence\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value-overriden\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value-overriden\")\n})", + "endpoint": "<<echoHoppBaseURL>>", + "testScript": "pw.test(\"Value set at the pre-request script takes precedence\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value-overriden\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"secret-header-key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value-overriden\")\n})", "preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)" }, { @@ -68,8 +68,8 @@ "params": [], "requestVariables": [], "headers": [], - "endpoint": "<<baseURL>>/post", - "testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})", + "endpoint": "<<echoHoppBaseURL>>/post", + "testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(JSON.parse(pw.response.body.data).secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})", "preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)" }, { @@ -93,7 +93,7 @@ ], "requestVariables": [], "headers": [], - "endpoint": "<<baseURL>>/get", + "endpoint": "<<echoHoppBaseURL>>", "testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})", "preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)" }, @@ -114,8 +114,8 @@ "params": [], "requestVariables": [], "headers": [], - "endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>", - "testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});", + "endpoint": "<<httpbinBaseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>", + "testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n // The endpoint at times results in a `502` bad gateway\n if (pw.response.status !== 200) {\n return\n }\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});", "preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}" }, { @@ -136,8 +136,8 @@ "params": [], "requestVariables": [], "headers": [], - "endpoint": "<<baseURL>>/bearer", - "testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});", + "endpoint": "<<httpbinBaseURL>>/bearer", + "testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n // Safeguard to prevent test failures due to the endpoint\n if (pw.response.status !== 200) {\n return\n }\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});", "preRequestScript": "let secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n\nif (!secretBearerToken) {\n pw.env.set(\"secretBearerToken\", \"test-token\")\n secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n}\n\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)" } ], @@ -146,4 +146,4 @@ "authActive": false }, "headers": [] -} +} \ No newline at end of file
packages/hoppscotch-cli/src/__tests__/samples/collections/secret-envs-persistence-scripting-coll.json+2 −2 modified@@ -5,7 +5,7 @@ "requests": [ { "v": "3", - "endpoint": "https://httpbin.org/post", + "endpoint": "https://echo.hoppscotch.io/post", "name": "req", "params": [], "headers": [ @@ -18,7 +18,7 @@ "method": "POST", "auth": { "authType": "none", "authActive": true }, "preRequestScript": "pw.env.set(\"preReqVarOne\", \"pre-req-value-one\")\n\npw.env.set(\"preReqVarTwo\", \"pre-req-value-two\")\n\npw.env.set(\"customHeaderValueFromSecretVar\", \"custom-header-secret-value\")\n\npw.env.set(\"customBodyValue\", \"custom-body-value\")", - "testScript": "pw.test(\"Secret environment value set from the pre-request script takes precedence\", () => {\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(\"pre-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the pre-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request headers that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"custom-header-secret-value\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request body that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.json.key).toBe(\"custom-body-value\")\n})\n\npw.test(\"Secret environment variable set from the post-request script takes precedence\", () => {\n pw.env.set(\"postReqVarOne\", \"post-req-value-one\")\n pw.expect(pw.env.get(\"postReqVarOne\")).toBe(\"post-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the post-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully removes environment variables via the pw.env.unset method\", () => {\n pw.env.unset(\"preReqVarOne\")\n pw.env.unset(\"postReqVarTwo\")\n\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(undefined)\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(undefined)\n})", + "testScript": "pw.test(\"Secret environment value set from the pre-request script takes precedence\", () => {\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(\"pre-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the pre-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request headers that are set in pre-request script\", () => {\n pw.expect(pw.response.body.headers[\"custom-header\"]).toBe(\"custom-header-secret-value\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request body that are set in pre-request script\", () => {\n pw.expect(JSON.parse(pw.response.body.data).key).toBe(\"custom-body-value\")\n})\n\npw.test(\"Secret environment variable set from the post-request script takes precedence\", () => {\n pw.env.set(\"postReqVarOne\", \"post-req-value-one\")\n pw.expect(pw.env.get(\"postReqVarOne\")).toBe(\"post-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the post-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully removes environment variables via the pw.env.unset method\", () => {\n pw.env.unset(\"preReqVarOne\")\n pw.env.unset(\"postReqVarTwo\")\n\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(undefined)\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(undefined)\n})", "body": { "contentType": "application/json", "body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
packages/hoppscotch-cli/src/__tests__/samples/environments/secret-envs.json+6 −1 modified@@ -32,7 +32,12 @@ "secret": true }, { - "key": "baseURL", + "key": "echoHoppBaseURL", + "value": "https://echo.hoppscotch.io", + "secret": false + }, + { + "key": "httpbinBaseURL", "value": "https://httpbin.org", "secret": false }
packages/hoppscotch-cli/src/__tests__/samples/environments/secret-supplied-values-envs.json+6 −1 modified@@ -38,7 +38,12 @@ "secret": true }, { - "key": "baseURL", + "key": "echoHoppBaseURL", + "value": "https://echo.hoppscotch.io", + "secret": false + }, + { + "key": "httpbinBaseURL", "value": "https://httpbin.org", "secret": false }
packages/hoppscotch-cli/src/__tests__/utils.ts+18 −11 modified@@ -3,15 +3,16 @@ import { resolve } from "path"; import { ExecResponse } from "./types"; -export const runCLI = (args: string, options = {}): Promise<ExecResponse> => - { - const CLI_PATH = resolve(__dirname, "../../bin/hopp"); - const command = `node ${CLI_PATH} ${args}` - - return new Promise((resolve) => - exec(command, options, (error, stdout, stderr) => resolve({ error, stdout, stderr })) - ); - } +export const runCLI = (args: string, options = {}): Promise<ExecResponse> => { + const CLI_PATH = resolve(__dirname, "../../bin/hopp.js"); + const command = `node ${CLI_PATH} ${args}`; + + return new Promise((resolve) => + exec(command, options, (error, stdout, stderr) => + resolve({ error, stdout, stderr }) + ) + ); +}; export const trimAnsi = (target: string) => { const ansiRegex = @@ -25,12 +26,18 @@ export const getErrorCode = (out: string) => { return ansiTrimmedStr.split(" ")[0]; }; -export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => { +export const getTestJsonFilePath = ( + file: string, + kind: "collection" | "environment" +) => { const kindDir = { collection: "collections", environment: "environments", }[kind]; - const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`); + const filePath = resolve( + __dirname, + `../../src/__tests__/samples/${kindDir}/${file}` + ); return filePath; };
packages/hoppscotch-cli/src/utils/mutators.ts+36 −18 modified@@ -7,6 +7,41 @@ import { error } from "../types/errors"; import { FormDataEntry } from "../types/request"; import { isHoppErrnoException } from "./checks"; +const getValidRequests = ( + collections: HoppCollection[], + collectionFilePath: string +) => { + return collections.map((collection) => { + // Validate requests using zod schema + const requestSchemaParsedResult = z + .array(entityReference(HoppRESTRequest)) + .safeParse(collection.requests); + + // Handle validation errors + if (!requestSchemaParsedResult.success) { + throw error({ + code: "MALFORMED_COLLECTION", + path: collectionFilePath, + data: "Please check the collection data.", + }); + } + + // Recursively validate requests in nested folders + if (collection.folders.length > 0) { + collection.folders = getValidRequests( + collection.folders, + collectionFilePath + ); + } + + // Return validated collection + return { + ...collection, + requests: requestSchemaParsedResult.data, + }; + }); +}; + /** * Parses array of FormDataEntry to FormData. * @param values Array of FormDataEntry. @@ -82,22 +117,5 @@ export async function parseCollectionData( }); } - return collectionSchemaParsedResult.data.map((collection) => { - const requestSchemaParsedResult = z - .array(entityReference(HoppRESTRequest)) - .safeParse(collection.requests); - - if (!requestSchemaParsedResult.success) { - throw error({ - code: "MALFORMED_COLLECTION", - path, - data: "Please check the collection data.", - }); - } - - return { - ...collection, - requests: requestSchemaParsedResult.data, - }; - }); + return getValidRequests(collectionSchemaParsedResult.data, path); }
packages/hoppscotch-js-sandbox/jest.config.js+0 −10 removed@@ -1,10 +0,0 @@ -export default { - preset: "ts-jest", - testEnvironment: "jsdom", - collectCoverage: true, - setupFilesAfterEnv: ["./jest.setup.ts"], - moduleNameMapper: { - "~/(.*)": "<rootDir>/src/$1", - "^lodash-es$": "lodash", - }, -}
packages/hoppscotch-js-sandbox/jest.setup.ts+0 −1 removed@@ -1 +0,0 @@ -require("@relmify/jest-fp-ts")
packages/hoppscotch-js-sandbox/node.d.ts+2 −2 modified@@ -1,2 +1,2 @@ -export { default } from "./dist/node.d.ts" -export * from "./dist/node.d.ts" +export { default } from "./dist/node/index.d.ts" +export * from "./dist/node/index.d.ts"
packages/hoppscotch-js-sandbox/package.json+11 −3 modified@@ -30,7 +30,7 @@ "scripts": { "lint": "eslint --ext .ts,.js --ignore-path .gitignore .", "lintfix": "eslint --fix --ext .ts,.js --ignore-path .gitignore .", - "test": "pnpm exec jest", + "test": "vitest run", "build": "vite build && tsc --emitDeclarationOnly", "clean": "pnpm tsc --build --clean", "postinstall": "pnpm run build", @@ -69,10 +69,18 @@ "eslint-config-prettier": "8.6.0", "eslint-plugin-prettier": "4.2.1", "io-ts": "2.2.16", - "jest": "27.5.1", "prettier": "2.8.4", "ts-jest": "27.1.5", "typescript": "4.9.5", - "vite": "5.0.5" + "vite": "5.0.5", + "vitest": "0.34.6" + }, + "peerDependencies": { + "isolated-vm": "4.7.2" + }, + "peerDependenciesMeta": { + "isolated-vm": { + "optional": true + } } } \ No newline at end of file
packages/hoppscotch-js-sandbox/setupFiles.ts+15 −0 added@@ -0,0 +1,15 @@ +// Vitest doesn't work without globals +// Ref: https://github.com/relmify/jest-fp-ts/issues/11 + +import decodeMatchers from "@relmify/jest-fp-ts/dist/decodeMatchers" +import eitherMatchers from "@relmify/jest-fp-ts/dist/eitherMatchers" +import optionMatchers from "@relmify/jest-fp-ts/dist/optionMatchers" +import theseMatchers from "@relmify/jest-fp-ts/dist/theseMatchers" +import eitherOrTheseMatchers from "@relmify/jest-fp-ts/dist/eitherOrTheseMatchers" +import { expect } from "vitest" + +expect.extend(decodeMatchers.matchers) +expect.extend(eitherMatchers.matchers) +expect.extend(optionMatchers.matchers) +expect.extend(theseMatchers.matchers) +expect.extend(eitherOrTheseMatchers.matchers)
packages/hoppscotch-js-sandbox/src/node/index.ts+2 −0 added@@ -0,0 +1,2 @@ +export { runPreRequestScript } from "./pre-request" +export { runTestScript } from "./test-runner"
packages/hoppscotch-js-sandbox/src/node/pre-request.ts+91 −0 added@@ -0,0 +1,91 @@ +import { pipe } from "fp-ts/function" +import * as TE from "fp-ts/lib/TaskEither" +import { createRequire } from "module" + +import type ivmT from "isolated-vm" + +import { TestResult } from "~/types" +import { getPreRequestScriptMethods } from "~/shared-utils" +import { getSerializedAPIMethods } from "./utils" + +const nodeRequire = createRequire(import.meta.url) +const ivm = nodeRequire("isolated-vm") + +export const runPreRequestScript = ( + preRequestScript: string, + envs: TestResult["envs"] +): TE.TaskEither<string, TestResult["envs"]> => + pipe( + TE.tryCatch( + async () => { + const isolate: ivmT.Isolate = new ivm.Isolate() + const context = await isolate.createContext() + return { isolate, context } + }, + (reason) => `Context initialization failed: ${reason}` + ), + TE.chain(({ isolate, context }) => + pipe( + TE.tryCatch( + async () => { + const jail = context.global + + const { pw, updatedEnvs } = getPreRequestScriptMethods(envs) + + const serializedAPIMethods = getSerializedAPIMethods(pw) + jail.setSync("serializedAPIMethods", serializedAPIMethods, { + copy: true, + }) + + jail.setSync("atob", atob) + jail.setSync("btoa", btoa) + + // Methods in the isolate context can't be invoked straightaway + const finalScript = ` + const pw = new Proxy(serializedAPIMethods, { + get: (pwObjTarget, pwObjProp) => { + const topLevelEntry = pwObjTarget[pwObjProp] + + // "pw.env" set of API methods + if (topLevelEntry && typeof topLevelEntry === "object") { + return new Proxy(topLevelEntry, { + get: (subTarget, subProp) => { + const subLevelProperty = subTarget[subProp] + if (subLevelProperty && subLevelProperty.typeof === "function") { + return (...args) => subLevelProperty.applySync(null, args) + } + }, + }) + } + } + }) + + ${preRequestScript} + ` + + // Create a script and compile it + const script = await isolate.compileScript(finalScript) + + // Run the pre-request script in the provided context + await script.run(context) + + return updatedEnvs + }, + (reason) => reason + ), + TE.fold( + (error) => TE.left(`Script execution failed: ${error}`), + (result) => + pipe( + TE.tryCatch( + async () => { + await isolate.dispose() + return result + }, + (disposeError) => `Isolate disposal failed: ${disposeError}` + ) + ) + ) + ) + ) + )
packages/hoppscotch-js-sandbox/src/node/test-runner.ts+217 −0 added@@ -0,0 +1,217 @@ +import * as E from "fp-ts/Either" +import * as TE from "fp-ts/TaskEither" +import { pipe } from "fp-ts/function" +import { createRequire } from "module" + +import type ivmT from "isolated-vm" + +import { TestResponse, TestResult } from "~/types" +import { + getTestRunnerScriptMethods, + preventCyclicObjects, +} from "~/shared-utils" +import { getSerializedAPIMethods } from "./utils" + +const nodeRequire = createRequire(import.meta.url) +const ivm = nodeRequire("isolated-vm") + +export const runTestScript = ( + testScript: string, + envs: TestResult["envs"], + response: TestResponse +): TE.TaskEither<string, TestResult> => + pipe( + TE.tryCatch( + async () => { + const isolate: ivmT.Isolate = new ivm.Isolate() + const context = await isolate.createContext() + return { isolate, context } + }, + (reason) => `Context initialization failed: ${reason}` + ), + TE.chain(({ isolate, context }) => + pipe( + TE.tryCatch( + async () => + executeScriptInContext( + testScript, + envs, + response, + isolate, + context + ), + (reason) => `Script execution failed: ${reason}` + ), + TE.chain((result) => + TE.tryCatch( + async () => { + await isolate.dispose() + return result + }, + (disposeReason) => `Isolate disposal failed: ${disposeReason}` + ) + ) + ) + ) + ) +const executeScriptInContext = ( + testScript: string, + envs: TestResult["envs"], + response: TestResponse, + isolate: ivmT.Isolate, + context: ivmT.Context +): Promise<TestResult> => { + return new Promise((resolve, reject) => { + // Parse response object + const responseObjHandle = preventCyclicObjects(response) + if (E.isLeft(responseObjHandle)) { + return reject(`Response parsing failed: ${responseObjHandle.left}`) + } + + const jail = context.global + + const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs) + + const serializedAPIMethods = getSerializedAPIMethods({ + ...pw, + response: responseObjHandle.right, + }) + jail.setSync("serializedAPIMethods", serializedAPIMethods, { copy: true }) + + jail.setSync("atob", atob) + jail.setSync("btoa", btoa) + + jail.setSync("ivm", ivm) + + // Methods in the isolate context can't be invoked straightaway + const finalScript = ` + const pw = new Proxy(serializedAPIMethods, { + get: (pwObj, pwObjProp) => { + // pw.expect(), pw.env, etc. + const topLevelEntry = pwObj[pwObjProp] + + // If the entry exists and is a function + // pw.expect(), pw.test(), etc. + if (topLevelEntry && topLevelEntry.typeof === "function") { + // pw.test() just involves invoking the function via "applySync()" + if (pwObjProp === "test") { + return (...args) => topLevelEntry.applySync(null, args) + } + + // pw.expect() returns an object with matcher methods + return (...args) => { + // Invoke "pw.expect()" and get access to the object with matcher methods + const expectFnResult = topLevelEntry.applySync( + null, + args.map((expectVal) => { + if (typeof expectVal === "object") { + if (expectVal === null) { + return null + } + + // Only arrays and objects stringified here should be parsed from the "pw.expect()" method definition + // The usecase is that any JSON string supplied should be preserved + // An extra "isStringifiedWithinIsolate" prop is added to indicate it has to be parsed + + if (Array.isArray(expectVal)) { + return JSON.stringify({ + arr: expectVal, + isStringifiedWithinIsolate: true, + }) + } + + return JSON.stringify({ + ...expectVal, + isStringifiedWithinIsolate: true, + }) + } + + return expectVal + }) + ) + + // Matcher methods that can be chained with "pw.expect()" + // pw.expect().toBe(), etc + if (expectFnResult.typeof === "object") { + // Access the getter that points to the negated matcher methods via "{ accessors: true }" + const matcherMethods = { + not: expectFnResult.getSync("not", { accessors: true }), + } + + // Serialize matcher methods for use in the isolate context + const matcherMethodNames = [ + "toBe", + "toBeLevel2xx", + "toBeLevel3xx", + "toBeLevel4xx", + "toBeLevel5xx", + "toBeType", + "toHaveLength", + "toInclude", + ] + matcherMethodNames.forEach((methodName) => { + matcherMethods[methodName] = expectFnResult.getSync(methodName) + }) + + return new Proxy(matcherMethods, { + get: (matcherMethodTarget, matcherMethodProp) => { + // pw.expect().not.toBe(), etc + const matcherMethodEntry = matcherMethodTarget[matcherMethodProp] + + if (matcherMethodProp === "not") { + return new Proxy(matcherMethodEntry, { + get: (negatedObjTarget, negatedObjprop) => { + // Return the negated matcher method defn that is invoked from the test script + const negatedMatcherMethodDefn = negatedObjTarget.getSync(negatedObjprop) + return negatedMatcherMethodDefn + }, + }) + } + + // Return the matcher method defn that is invoked from the test script + return matcherMethodEntry + }, + }) + } + } + } + + // "pw.env" set of API methods + if (typeof topLevelEntry === "object" && pwObjProp !== "response") { + return new Proxy(topLevelEntry, { + get: (subTarget, subProp) => { + const subLevelProperty = subTarget[subProp] + if ( + subLevelProperty && + subLevelProperty.typeof === "function" + ) { + return (...args) => subLevelProperty.applySync(null, args) + } + }, + }) + } + + return topLevelEntry + }, + }) + + ${testScript} + ` + + // Create a script and compile it + const script = isolate.compileScript(finalScript) + + // Run the test script in the provided context + script + .then((script) => script.run(context)) + .then(() => { + resolve({ + tests: testRunStack, + envs: updatedEnvs, + }) + }) + .catch((error: Error) => { + reject(error) + }) + }) +}
packages/hoppscotch-js-sandbox/src/node.ts+0 −2 removed@@ -1,2 +0,0 @@ -export * from "./pre-request/node-vm" -export * from "./test-runner/node-vm"
packages/hoppscotch-js-sandbox/src/node/utils.ts+23 −0 added@@ -0,0 +1,23 @@ +import { createRequire } from "module" + +const nodeRequire = createRequire(import.meta.url) +const ivm = nodeRequire("isolated-vm") + +// Helper function to recursively wrap methods in `ivm.Reference` +export const getSerializedAPIMethods = ( + namespaceObj: Record<string, unknown> +): Record<string, unknown> => { + const result: Record<string, unknown> = {} + + for (const [key, value] of Object.entries(namespaceObj)) { + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + result[key] = getSerializedAPIMethods(value as Record<string, unknown>) + } else if (typeof value === "function") { + result[key] = new ivm.Reference(value) + } else { + result[key] = value + } + } + + return result +}
packages/hoppscotch-js-sandbox/src/pre-request/node-vm/index.ts+0 −38 removed@@ -1,38 +0,0 @@ -import { pipe } from "fp-ts/function" -import * as TE from "fp-ts/lib/TaskEither" -import { createContext, runInContext } from "vm" - -import { TestResult } from "~/types" -import { getPreRequestScriptMethods } from "~/utils" - -export const runPreRequestScript = ( - preRequestScript: string, - envs: TestResult["envs"] -): TE.TaskEither<string, TestResult["envs"]> => - pipe( - TE.tryCatch( - async () => { - return createContext() - }, - (reason) => `Context initialization failed: ${reason}` - ), - TE.chain((context) => - TE.tryCatch( - () => - new Promise((resolve) => { - const { pw, updatedEnvs } = getPreRequestScriptMethods(envs) - - // Expose pw to the context - context.pw = pw - context.atob = atob - context.btoa = btoa - - // Run the pre-request script in the provided context - runInContext(preRequestScript, context) - - resolve(updatedEnvs) - }), - (reason) => `Script execution failed: ${reason}` - ) - ) - )
packages/hoppscotch-js-sandbox/src/shared-utils.ts+54 −11 renamed@@ -182,6 +182,36 @@ const getSharedMethods = (envs: TestResult["envs"]) => { } } +const getResolvedExpectValue = (expectVal: any) => { + if (typeof expectVal !== "string") { + return expectVal + } + + try { + const parsedExpectVal = JSON.parse(expectVal) + + // Supplying non-primitive values is not permitted in the `isStringifiedWithinIsolate` property indicates that the object was stringified before executing the script from the isolate context + // This is done to ensure a JSON string supplied as the "expectVal" is not parsed and preserved as is + if (typeof parsedExpectVal === "object") { + if (parsedExpectVal.isStringifiedWithinIsolate !== true) { + return expectVal + } + + // For an array, the contents are stored in the `arr` property + if (Array.isArray(parsedExpectVal.arr)) { + return parsedExpectVal.arr + } + + delete parsedExpectVal.isStringifiedWithinIsolate + return parsedExpectVal + } + + return expectVal + } catch (_) { + return expectVal + } +} + export function preventCyclicObjects( obj: Record<string, any> ): E.Left<string> | E.Right<Record<string, any>> { @@ -215,15 +245,18 @@ export const createExpectation = ( ) => { const result: Record<string, unknown> = {} + // Non-primitive values supplied are stringified in the isolate context + const resolvedExpectVal = getResolvedExpectValue(expectVal) + const toBeFn = (expectedVal: any) => { - let assertion = expectVal === expectedVal + let assertion = resolvedExpectVal === expectedVal if (negated) { assertion = !assertion } const status = assertion ? "pass" : "fail" - const message = `Expected '${expectVal}' to${ + const message = `Expected '${resolvedExpectVal}' to${ negated ? " not" : "" } be '${expectedVal}'` @@ -240,7 +273,7 @@ export const createExpectation = ( rangeStart: number, rangeEnd: number ) => { - const parsedExpectVal = parseInt(expectVal) + const parsedExpectVal = parseInt(resolvedExpectVal) if (!Number.isNaN(parsedExpectVal)) { let assertion = @@ -260,7 +293,7 @@ export const createExpectation = ( message, }) } else { - const message = `Expected ${level}-level status but could not parse value '${expectVal}'` + const message = `Expected ${level}-level status but could not parse value '${resolvedExpectVal}'` currTestStack[currTestStack.length - 1].expectResults.push({ status: "error", message, @@ -288,14 +321,14 @@ export const createExpectation = ( "function", ].includes(expectedType) ) { - let assertion = typeof expectVal === expectedType + let assertion = typeof resolvedExpectVal === expectedType if (negated) { assertion = !assertion } const status = assertion ? "pass" : "fail" - const message = `Expected '${expectVal}' to${ + const message = `Expected '${resolvedExpectVal}' to${ negated ? " not" : "" } be type '${expectedType}'` @@ -316,7 +349,12 @@ export const createExpectation = ( } const toHaveLengthFn = (expectedLength: any) => { - if (!(Array.isArray(expectVal) || typeof expectVal === "string")) { + if ( + !( + Array.isArray(resolvedExpectVal) || + typeof resolvedExpectVal === "string" + ) + ) { const message = "Expected toHaveLength to be called for an array or string" currTestStack[currTestStack.length - 1].expectResults.push({ @@ -328,7 +366,7 @@ export const createExpectation = ( } if (typeof expectedLength === "number" && !Number.isNaN(expectedLength)) { - let assertion = expectVal.length === expectedLength + let assertion = resolvedExpectVal.length === expectedLength if (negated) { assertion = !assertion @@ -355,7 +393,12 @@ export const createExpectation = ( } const toIncludeFn = (needle: any) => { - if (!(Array.isArray(expectVal) || typeof expectVal === "string")) { + if ( + !( + Array.isArray(resolvedExpectVal) || + typeof resolvedExpectVal === "string" + ) + ) { const message = "Expected toInclude to be called for an array or string" currTestStack[currTestStack.length - 1].expectResults.push({ status: "error", @@ -382,13 +425,13 @@ export const createExpectation = ( return undefined } - let assertion = expectVal.includes(needle) + let assertion = resolvedExpectVal.includes(needle) if (negated) { assertion = !assertion } - const expectValPretty = JSON.stringify(expectVal) + const expectValPretty = JSON.stringify(resolvedExpectVal) const needlePretty = JSON.stringify(needle) const status = assertion ? "pass" : "fail" const message = `Expected ${expectValPretty} to${
packages/hoppscotch-js-sandbox/src/test-runner/node-vm/index.ts+0 −57 removed@@ -1,57 +0,0 @@ -import * as E from "fp-ts/Either" -import * as TE from "fp-ts/TaskEither" -import { pipe } from "fp-ts/function" -import { createContext, runInContext } from "vm" - -import { TestResponse, TestResult } from "~/types" -import { getTestRunnerScriptMethods, preventCyclicObjects } from "~/utils" - -export const runTestScript = ( - testScript: string, - envs: TestResult["envs"], - response: TestResponse -): TE.TaskEither<string, TestResult> => - pipe( - TE.tryCatch( - async () => { - return createContext() - }, - (reason) => `Context initialization failed: ${reason}` - ), - TE.chain((context) => - TE.tryCatch( - () => executeScriptInContext(testScript, envs, response, context), - (reason) => `Script execution failed: ${reason}` - ) - ) - ) - -const executeScriptInContext = ( - testScript: string, - envs: TestResult["envs"], - response: TestResponse, - context: any -): Promise<TestResult> => { - return new Promise((resolve, reject) => { - // Parse response object - const responseObjHandle = preventCyclicObjects(response) - if (E.isLeft(responseObjHandle)) { - return reject(`Response parsing failed: ${responseObjHandle.left}`) - } - - const { pw, testRunStack, updatedEnvs } = getTestRunnerScriptMethods(envs) - - // Expose pw to the context - context.pw = { ...pw, response: responseObjHandle.right } - context.atob = atob - context.btoa = btoa - - // Run the test script in the provided context - runInContext(testScript, context) - - resolve({ - tests: testRunStack, - envs: updatedEnvs, - }) - }) -}
packages/hoppscotch-js-sandbox/src/__tests__/base64-helper-functions.spec.ts+3 −2 renamed@@ -1,8 +1,9 @@ import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runPreRequestScript } from "~/pre-request/node-vm" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runPreRequestScript, runTestScript } from "~/node" import { TestResponse, TestResult } from "~/types" describe("Base64 helper functions", () => {
packages/hoppscotch-js-sandbox/src/__tests__/env/getResolve.spec.ts+3 −2 renamed@@ -1,8 +1,9 @@ -import "@relmify/jest-fp-ts" import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse, TestResult } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/__tests__/env/get.spec.ts+3 −2 renamed@@ -1,8 +1,9 @@ -import "@relmify/jest-fp-ts" import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse, TestResult } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/__tests__/env/resolve.spec.ts+3 −1 renamed@@ -1,7 +1,9 @@ import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse, TestResult } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/__tests__/env/set.spec.ts+3 −1 renamed@@ -1,7 +1,9 @@ import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse, TestResult } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/__tests__/env/unset.spec.ts+3 −1 renamed@@ -1,7 +1,9 @@ import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse, TestResult } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/__tests__/expect/toBeLevelxxx.spec.ts+3 −2 renamed@@ -1,8 +1,9 @@ -import "@relmify/jest-fp-ts" import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/__tests__/expect/toBe.spec.ts+4 −3 renamed@@ -1,8 +1,9 @@ -import "@relmify/jest-fp-ts" import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse } from "~/types" const fakeResponse: TestResponse = { @@ -23,7 +24,7 @@ describe("toBe", () => { return expect( func( ` - pw.expect(2).toBe(2) + pw.expect(2).toBe(2) `, fakeResponse )()
packages/hoppscotch-js-sandbox/src/__tests__/expect/toBeType.spec.ts+3 −1 renamed@@ -1,7 +1,9 @@ import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/__tests__/expect/toHaveLength.spec.ts+3 −1 renamed@@ -1,7 +1,9 @@ import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/__tests__/expect/toInclude.spec.ts+3 −1 renamed@@ -1,7 +1,9 @@ import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/__tests__/pre-request.spec.ts+2 −2 renamed@@ -1,6 +1,6 @@ -import "@relmify/jest-fp-ts" +import { describe, expect, test } from "vitest" -import { runPreRequestScript } from "~/pre-request/node-vm" +import { runPreRequestScript } from "~/node" describe("runPreRequestScript", () => { test("returns the updated environment properly", () => {
packages/hoppscotch-js-sandbox/src/__tests__/shared-utils.spec.ts+3 −1 renamed@@ -1,4 +1,6 @@ -import { preventCyclicObjects } from "~/utils" +import { preventCyclicObjects } from "~/shared-utils" + +import { describe, expect, test } from "vitest" describe("preventCyclicObjects", () => { test("succeeds with a simple object", () => {
packages/hoppscotch-js-sandbox/src/__tests__/test-runner.spec.ts+3 −1 renamed@@ -1,7 +1,9 @@ import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" -import { runTestScript } from "~/test-runner/node-vm" +import { describe, expect, test } from "vitest" + +import { runTestScript } from "~/node" import { TestResponse } from "~/types" const fakeResponse: TestResponse = {
packages/hoppscotch-js-sandbox/src/web/index.ts+2 −0 added@@ -0,0 +1,2 @@ +export { runPreRequestScript } from "./pre-request" +export { runTestScript } from "./test-runner"
packages/hoppscotch-js-sandbox/src/web/pre-request/index.ts+0 −0 renamedpackages/hoppscotch-js-sandbox/src/web/pre-request/worker.ts+1 −1 renamed@@ -1,7 +1,7 @@ import * as TE from "fp-ts/TaskEither" import { TestResult } from "~/types" -import { getPreRequestScriptMethods } from "~/utils" +import { getPreRequestScriptMethods } from "~/shared-utils" const executeScriptInContext = ( preRequestScript: string,
packages/hoppscotch-js-sandbox/src/web/test-runner/index.ts+0 −0 renamedpackages/hoppscotch-js-sandbox/src/web/test-runner/worker.ts+4 −1 renamed@@ -2,7 +2,10 @@ import * as E from "fp-ts/Either" import * as TE from "fp-ts/TaskEither" import { SandboxTestResult, TestResponse, TestResult } from "~/types" -import { getTestRunnerScriptMethods, preventCyclicObjects } from "~/utils" +import { + getTestRunnerScriptMethods, + preventCyclicObjects, +} from "~/shared-utils" const executeScriptInContext = ( testScript: string,
packages/hoppscotch-js-sandbox/src/web.ts+0 −2 removed@@ -1,2 +0,0 @@ -export * from "./pre-request/web-worker" -export * from "./test-runner/web-worker"
packages/hoppscotch-js-sandbox/vite.config.ts+7 −3 modified@@ -7,16 +7,20 @@ export default defineConfig({ emptyOutDir: true, lib: { entry: { - web: "./src/web.ts", - node: "./src/node.ts", + web: "./src/web/index.ts", + node: "./src/node/index.ts", }, name: "js-sandbox", formats: ["es", "cjs"], }, rollupOptions: { - external: ["vm"], + external: ["module"], }, }, + test: { + environment: "node", + setupFiles: ["./setupFiles.ts"], + }, resolve: { alias: { "~": resolve(__dirname, "./src"),
packages/hoppscotch-js-sandbox/web.d.ts+2 −2 modified@@ -1,2 +1,2 @@ -export { default } from "./dist/web.d.ts" -export * from "./dist/web.d.ts" +export { default } from "./dist/web/index.d.ts" +export * from "./dist/web/index.d.ts"
pnpm-lock.yaml+165 −19 modified@@ -309,6 +309,9 @@ importers: commander: specifier: 11.1.0 version: 11.1.0 + isolated-vm: + specifier: 4.7.2 + version: 4.7.2 lodash-es: specifier: 4.17.21 version: 4.17.21 @@ -828,6 +831,9 @@ importers: fp-ts: specifier: 2.12.1 version: 2.12.1 + isolated-vm: + specifier: 4.7.2 + version: 4.7.2 lodash: specifier: 4.17.21 version: 4.17.21 @@ -868,9 +874,6 @@ importers: io-ts: specifier: 2.2.16 version: 2.2.16(fp-ts@2.12.1) - jest: - specifier: 27.5.1 - version: 27.5.1 prettier: specifier: 2.8.4 version: 2.8.4 @@ -883,6 +886,9 @@ importers: vite: specifier: 5.0.5 version: 5.0.5(@types/node@17.0.45)(terser@5.27.0) + vitest: + specifier: 0.34.6 + version: 0.34.6(sass@1.69.5)(terser@5.27.0) packages/hoppscotch-selfhost-desktop: dependencies: @@ -3061,7 +3067,7 @@ packages: peerDependencies: vue: 3.3.9 dependencies: - vue: 3.3.9(typescript@5.3.2) + vue: 3.3.9(typescript@4.9.5) /@codemirror/autocomplete@6.13.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.25.1)(@lezer/common@1.2.1): resolution: {integrity: sha512-SuDrho1klTINfbcMPnyro1ZxU9xJtwDMtb62R8TjL/tOl71IoOsvBo1a9x+hDvHhIzkTcJHy2VC+rmpGgYkRSw==} @@ -6120,7 +6126,7 @@ packages: peerDependencies: vue: 3.3.9 dependencies: - vue: 3.3.9(typescript@5.3.2) + vue: 3.3.9(typescript@4.9.5) /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} @@ -8396,7 +8402,7 @@ packages: /@types/graceful-fs@4.1.5: resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} dependencies: - '@types/node': 18.18.8 + '@types/node': 17.0.45 dev: true /@types/har-format@1.2.15: @@ -9621,7 +9627,7 @@ packages: regenerator-runtime: 0.13.11 systemjs: 6.14.2 terser: 5.27.0 - vite: 3.2.4(@types/node@18.18.8)(sass@1.58.0)(terser@5.27.0) + vite: 3.2.4(@types/node@17.0.27)(terser@5.27.0) /@vitejs/plugin-legacy@2.3.0(terser@5.27.0)(vite@4.5.0): resolution: {integrity: sha512-Bh62i0gzQvvT8AeAAb78nOnqSYXypkRmQmOTImdPZ39meHR9e2une3AIFmVo4s1SDmcmJ6qj18Sa/lRc/14KaA==} @@ -10143,7 +10149,7 @@ packages: '@types/web-bluetooth': 0.0.14 '@vueuse/metadata': 8.7.5 '@vueuse/shared': 8.7.5(vue@3.3.9) - vue: 3.3.9(typescript@5.3.2) + vue: 3.3.9(typescript@4.9.5) vue-demi: 0.14.6(vue@3.3.9) /@vueuse/core@9.12.0(vue@3.3.9): @@ -10202,7 +10208,7 @@ packages: vue: optional: true dependencies: - vue: 3.3.9(typescript@5.3.2) + vue: 3.3.9(typescript@4.9.5) vue-demi: 0.14.6(vue@3.3.9) /@vueuse/shared@9.12.0(vue@3.3.9): @@ -11120,7 +11126,6 @@ packages: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.0 - dev: true /blob@0.0.5: resolution: {integrity: sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==} @@ -11257,7 +11262,6 @@ packages: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: true /buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -11519,6 +11523,10 @@ packages: optionalDependencies: fsevents: 2.3.3 + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -12233,6 +12241,13 @@ packages: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: true + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + /dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true @@ -12627,7 +12642,6 @@ packages: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: once: 1.4.0 - dev: true /engine.io-client@3.5.3: resolution: {integrity: sha512-qsgyc/CEhJ6cgMUwxRRtOndGVhIu5hpL5tR4umSpmX/MvkFoIxUTM7oFMDQumHNzlNLwSVy6qhstFPoWTf7dOw==} @@ -13991,6 +14005,11 @@ packages: engines: {node: '>= 0.8.0'} dev: true + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + /expect@27.5.1: resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -14409,6 +14428,10 @@ packages: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} dev: false + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + /fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -14592,6 +14615,10 @@ packages: through2: 4.0.2 dev: true + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -15969,6 +15996,14 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isolated-vm@4.7.2: + resolution: {integrity: sha512-JVEs5gzWObzZK5+OlBplCdYSpokMcdhLSs/xWYYxmYWVfOOFF4oZJsYh7E/FmfX8e7gMioXMpMMeEyX1afuKrg==} + engines: {node: '>=16.0.0'} + requiresBuild: true + dependencies: + prebuild-install: 7.1.2 + dev: false + /isomorphic-fetch@3.0.0: resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} dependencies: @@ -16954,7 +16989,7 @@ packages: jest-util: 27.5.1 natural-compare: 1.4.0 pretty-format: 27.5.1 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color dev: true @@ -17099,7 +17134,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.18.8 + '@types/node': 17.0.45 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -18004,6 +18039,11 @@ packages: engines: {node: '>=12'} dev: true + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -18446,6 +18486,10 @@ packages: - encoding dev: false + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -18548,6 +18592,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} dev: true @@ -18582,6 +18630,13 @@ packages: lower-case: 2.0.2 tslib: 2.6.2 + /node-abi@3.57.0: + resolution: {integrity: sha512-Dp+A9JWxRaKuHP35H77I4kCKesDy5HUDEmScia2FyncMTOXASMyg251F5PhFoDA5uqBrDDffiLpbqnrZmNXW+g==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + dev: false + /node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} @@ -19457,6 +19512,25 @@ packages: punycode: 2.3.0 dev: false + /prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.1 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.6 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.57.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} @@ -19819,7 +19893,6 @@ packages: dependencies: end-of-stream: 1.4.4 once: 1.4.0 - dev: true /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -20577,7 +20650,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -20747,6 +20819,18 @@ packages: resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} dev: true + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + /sirv@2.0.3: resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==} engines: {node: '>= 10'} @@ -21467,6 +21551,26 @@ packages: engines: {node: '>=6'} dev: true + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: false + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: false + /tar@6.1.13: resolution: {integrity: sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==} engines: {node: '>=10'} @@ -22137,6 +22241,12 @@ packages: typescript: 4.9.5 dev: true + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} engines: {node: '>= 0.8.0'} @@ -23031,7 +23141,7 @@ packages: '@types/eslint': 8.56.2 eslint: 8.57.0 rollup: 2.79.1 - vite: 3.2.4(@types/node@18.18.8)(sass@1.58.0)(terser@5.27.0) + vite: 3.2.4(@types/node@17.0.27)(terser@5.27.0) /vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@4.5.0): resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} @@ -23325,6 +23435,40 @@ packages: - supports-color dev: true + /vite@3.2.4(@types/node@17.0.27)(terser@5.27.0): + resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 17.0.27 + esbuild: 0.15.18 + postcss: 8.4.32 + resolve: 1.22.8 + rollup: 2.79.1 + terser: 5.27.0 + optionalDependencies: + fsevents: 2.3.3 + /vite@3.2.4(@types/node@18.18.8)(sass@1.58.0)(terser@5.27.0): resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -23746,7 +23890,7 @@ packages: '@vue/composition-api': optional: true dependencies: - vue: 3.3.9(typescript@5.3.2) + vue: 3.3.9(typescript@4.9.5) /vue-eslint-parser@9.3.1(eslint@8.47.0): resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==} @@ -23978,7 +24122,7 @@ packages: vue: 3.3.9 dependencies: sortablejs: 1.14.0 - vue: 3.3.9(typescript@5.3.2) + vue: 3.3.9(typescript@4.9.5) /w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} @@ -24462,6 +24606,7 @@ packages: /workbox-google-analytics@6.6.0: resolution: {integrity: sha512-p4DJa6OldXWd6M9zRl0H6vB9lkrmqYFkRQ2xEiNdBFp9U0LhsGO7hsBscVEyH9H2/3eZZt8c97NB2FD9U2NJ+Q==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 6.6.0 workbox-core: 6.6.0 @@ -24471,6 +24616,7 @@ packages: /workbox-google-analytics@7.0.0: resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} + deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained dependencies: workbox-background-sync: 7.0.0 workbox-core: 7.0.0
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-qmmm-73r2-f8xrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-34347ghsaADVISORY
- github.com/hoppscotch/hoppscotch/commit/22c6eabd133195d22874250a5ae40cb26b851b01nvdWEB
- github.com/hoppscotch/hoppscotch/security/advisories/GHSA-qmmm-73r2-f8xrnvdWEB
- www.sonarsource.com/blog/scripting-outside-the-box-api-client-security-risks-part-2nvdWEB
News mentions
0No linked articles in our index yet.