diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml
index b5867bcf056f..517333012e8f 100644
--- a/.github/workflows/release-build.yml
+++ b/.github/workflows/release-build.yml
@@ -141,7 +141,7 @@ jobs:
if: runner.os == 'Windows'
shell: bash
run: |
- smctl credentials save ${SM_API_KEY} ${SM_CLIENT_CERT_PASSWORD}
+ # smctl credentials save ${SM_API_KEY} ${SM_CLIENT_CERT_PASSWORD}
NODE_OPTIONS='--max_old_space_size=6144' npm run package:windows:unpacked -w insomnia
env:
SM_HOST: ${{ vars.DIGICERT_SM_HOST }}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 914fc1596c06..184ba76b1bdd 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -43,5 +43,6 @@
],
"[cpp]": {
"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"
- }
+ },
+ "editor.formatOnPaste": true
}
diff --git a/package-lock.json b/package-lock.json
index 2b265c50ca31..571c4f14f4e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11460,6 +11460,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
+ "node_modules/acorn-walk": {
+ "version": "8.3.5",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+ "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -11579,6 +11591,29 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/apache-crypt": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmmirror.com/apache-crypt/-/apache-crypt-1.2.6.tgz",
+ "integrity": "sha512-072WetlM4blL8PREJVeY+WHiUh1R5VNt2HfceGS8aKqttPHcmqE5pkKuXPz/ULmJOFkc8Hw3kfKl6vy7Qka6DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unix-crypt-td-js": "^1.1.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/apache-md5": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmmirror.com/apache-md5/-/apache-md5-1.1.8.tgz",
+ "integrity": "sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/apiconnect-wsdl": {
"version": "2.0.36",
"resolved": "https://registry.npmjs.org/apiconnect-wsdl/-/apiconnect-wsdl-2.0.36.tgz",
@@ -11906,6 +11941,29 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-uniq": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-uniq": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/array-uniq/-/array-uniq-1.0.3.tgz",
+ "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/array.prototype.findlast": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
@@ -12009,6 +12067,16 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
+ "node_modules/asn1": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz",
+ "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
"node_modules/assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
@@ -12229,6 +12297,13 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
+ "node_modules/bcryptjs": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz",
+ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
@@ -13680,6 +13755,16 @@
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
+ "node_modules/crypto-random-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
+ "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/css-in-js-utils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
@@ -13791,6 +13876,16 @@
"uuid": "dist/bin/uuid"
}
},
+ "node_modules/daemonize-process": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmmirror.com/daemonize-process/-/daemonize-process-1.0.9.tgz",
+ "integrity": "sha512-YoB+AmcgHIBDVeyfVWSCV90FNk799zX8Uvn7RJTDCD8Y0EMNbSfIKLG961VgchJme2GHmqpXUuV8Rxe2j2L+bw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
@@ -14496,6 +14591,19 @@
"node": "*"
}
},
+ "node_modules/dir-glob": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-2.2.2.tgz",
+ "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/dmg-builder": {
"version": "26.8.1",
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz",
@@ -16924,6 +17032,31 @@
"micromatch": "^4.0.2"
}
},
+ "node_modules/fixturez": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/fixturez/-/fixturez-1.1.0.tgz",
+ "integrity": "sha512-c4q9eZsAmCzj9gkrEO/YwIRlrHWt/TXQiX9jR9WeLFOqeeV6EyzdiiV28CpSzF6Ip+gyYrSv5UeOHqyzfcNTVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fs-extra": "^5.0.0",
+ "globby": "^7.1.1",
+ "signal-exit": "^3.0.2",
+ "tempy": "^0.2.1"
+ }
+ },
+ "node_modules/fixturez/node_modules/fs-extra": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-5.0.0.tgz",
+ "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
"node_modules/flat": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
@@ -17363,6 +17496,108 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/git-http-mock-server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/git-http-mock-server/-/git-http-mock-server-2.0.0.tgz",
+ "integrity": "sha512-LOCls7jjuzwfKmUbcFsqj2yIEqExBzv0rA1tL7j1ULhRLAax4U1Bd/rbU9ebtri1ldzgcPD1VAyuhS1pvDC2pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "^2.0.0",
+ "buffer-equal-constant-time": "^1.0.1",
+ "chalk": "^2.4.1",
+ "daemonize-process": "^1.0.9",
+ "fixturez": "^1.1.0",
+ "htpasswd-js": "^1.0.2",
+ "micro-cors": "^0.1.1",
+ "minimisted": "^2.0.0",
+ "ssh-keygen": "^0.4.2",
+ "ssh2": "^0.6.1",
+ "tree-kill": "^1.2.0"
+ },
+ "bin": {
+ "git-http-mock-server": "http-daemon.js",
+ "git-ssh-mock-server": "ssh-daemon.js"
+ }
+ },
+ "node_modules/git-http-mock-server/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/git-http-mock-server/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/git-http-mock-server/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/git-http-mock-server/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/git-http-mock-server/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/git-http-mock-server/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/git-http-mock-server/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -17473,6 +17708,51 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/globby": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmmirror.com/globby/-/globby-7.1.1.tgz",
+ "integrity": "sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^1.0.1",
+ "dir-glob": "^2.0.0",
+ "glob": "^7.1.2",
+ "ignore": "^3.3.5",
+ "pify": "^3.0.0",
+ "slash": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globby/node_modules/ignore": {
+ "version": "3.3.10",
+ "resolved": "https://registry.npmmirror.com/ignore/-/ignore-3.3.10.tgz",
+ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/globby/node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globby/node_modules/slash": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/slash/-/slash-1.0.0.tgz",
+ "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/gonzales-pe": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz",
@@ -18021,6 +18301,32 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/htpasswd-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/htpasswd-js/-/htpasswd-js-1.0.2.tgz",
+ "integrity": "sha512-KON5L4YKYXk647tmVclKgmHHG5nApjy9K+WiRoScnoWhS63lMoTca1ommUW2XQ3FDW8TtNDIQA7J0WYXICbMAA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "apache-crypt": "^1.2.1",
+ "apache-md5": "^1.1.2",
+ "bcryptjs": "^2.4.3",
+ "fs-extra": "^4.0.2",
+ "xerror": "^1.1.2"
+ }
+ },
+ "node_modules/htpasswd-js/node_modules/fs-extra": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-4.0.3.tgz",
+ "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
"node_modules/http-assert": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz",
@@ -20947,6 +21253,16 @@
"node": ">= 0.6"
}
},
+ "node_modules/micro-cors": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmmirror.com/micro-cors/-/micro-cors-0.1.1.tgz",
+ "integrity": "sha512-6WqIahA5sbQR1Gjexp1VuWGFDKbZZleJb/gy1khNGk18a6iN1FdTcr3Q8twaxkV5H94RjxIBjirYbWCehpMBFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -23342,6 +23658,29 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-type/node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -25967,6 +26306,60 @@
"license": "BSD-3-Clause",
"optional": true
},
+ "node_modules/ssh-keygen": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmmirror.com/ssh-keygen/-/ssh-keygen-0.4.2.tgz",
+ "integrity": "sha512-SlEWW3cCtz87jwtCTfxo+tR+SQd4jJXWaBI/D9JVd74b2/N9ZvrWcd9lMFwFv0iMYb4aVAeMderH4AK5ZyW+Nw==",
+ "dev": true,
+ "dependencies": {
+ "underscore": "1.4.x"
+ },
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/ssh-keygen/node_modules/underscore": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmmirror.com/underscore/-/underscore-1.4.4.tgz",
+ "integrity": "sha512-ZqGrAgaqqZM7LGRzNjLnw5elevWb5M8LEoDMadxIW3OWbcv72wMMgKdwOKpd5Fqxe8choLD8HN3iSj3TUh/giQ==",
+ "dev": true
+ },
+ "node_modules/ssh2": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmmirror.com/ssh2/-/ssh2-0.6.2.tgz",
+ "integrity": "sha512-DJ+dOhXEEsmNpcQTI0x69FS++JH6qqL/ltEHf01pI1SSLMAcmD+hL4jRwvHjPwynPsmSUbHJ/WIZYzROfqZWjA==",
+ "dev": true,
+ "dependencies": {
+ "ssh2-streams": "~0.2.0"
+ },
+ "engines": {
+ "node": ">=4.5.0"
+ }
+ },
+ "node_modules/ssh2-streams": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmmirror.com/ssh2-streams/-/ssh2-streams-0.2.1.tgz",
+ "integrity": "sha512-3zCOsmunh1JWgPshfhKmBCL3lUtHPoh+a/cyQ49Ft0Q0aF7xgN06b76L+oKtFi0fgO57FLjFztb1GlJcEZ4a3Q==",
+ "dev": true,
+ "dependencies": {
+ "asn1": "~0.2.0",
+ "semver": "^5.1.0",
+ "streamsearch": "~0.1.2"
+ },
+ "engines": {
+ "node": ">=4.5.0"
+ }
+ },
+ "node_modules/ssh2-streams/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
"node_modules/ssri": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz",
@@ -26216,6 +26609,15 @@
"any-promise": "^1.1.0"
}
},
+ "node_modules/streamsearch": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-0.1.2.tgz",
+ "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -26823,6 +27225,16 @@
"node": ">=6.0.0"
}
},
+ "node_modules/temp-dir": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/temp-dir/-/temp-dir-1.0.0.tgz",
+ "integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/temp-file": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
@@ -26886,6 +27298,20 @@
"rimraf": "bin.js"
}
},
+ "node_modules/tempy": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmmirror.com/tempy/-/tempy-0.2.1.tgz",
+ "integrity": "sha512-LB83o9bfZGrntdqPuRdanIVCPReam9SOZKW0fOy5I9X3A854GGWi0tjCqoXEk84XIEYBc/x9Hq3EFop/H5wJaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "temp-dir": "^1.0.0",
+ "unique-string": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/throttle-debounce": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz",
@@ -27109,6 +27535,16 @@
"node": ">=18"
}
},
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
"node_modules/truncate-utf8-bytes": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@@ -27515,6 +27951,19 @@
"node": "^18.17.0 || >=20.5.0"
}
},
+ "node_modules/unique-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/unique-string/-/unique-string-1.0.0.tgz",
+ "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "crypto-random-string": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@@ -27525,6 +27974,13 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/unix-crypt-td-js": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmmirror.com/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz",
+ "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -28840,6 +29296,13 @@
}
}
},
+ "node_modules/xerror": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/xerror/-/xerror-1.1.3.tgz",
+ "integrity": "sha512-2l5hmDymDUIuKT53v/nYxofTMUDQuu5P/Y3qHOjQiih6QUHBCgWpbpL3I8BoE5TVfUVTMmUQ0jdUAimTGc9UIg==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
@@ -29154,7 +29617,7 @@
}
},
"packages/insomnia": {
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"license": "Apache-2.0",
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.6",
@@ -29189,6 +29652,8 @@
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-virtual": "3.13.12",
"@xmldom/xmldom": "^0.9.8",
+ "acorn": "^8.16.0",
+ "acorn-walk": "^8.3.5",
"ajv": "^8.17.1",
"apiconnect-wsdl": "2.0.36",
"aws4": "^1.13.2",
@@ -29332,14 +29797,14 @@
}
},
"packages/insomnia-api": {
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"license": "Apache-2.0",
"dependencies": {
"@getinsomnia/insomnia-v3-fetch": "^1.0.1"
}
},
"packages/insomnia-inso": {
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"license": "Apache-2.0",
"dependencies": {
"@seald-io/nedb": "^4.1.1",
@@ -29388,7 +29853,7 @@
"license": "MIT"
},
"packages/insomnia-scripting-environment": {
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"license": "Apache-2.0",
"dependencies": {
"@types/deep-equal": "^1.0.4",
@@ -29408,7 +29873,7 @@
}
},
"packages/insomnia-smoke-test": {
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"license": "Apache-2.0",
"devDependencies": {
"@playwright/test": "1.55.1",
@@ -29421,6 +29886,7 @@
"esbuild-runner": "2.2.2",
"express": "^4.21.2",
"express-basic-auth": "^1.2.1",
+ "git-http-mock-server": "^2.0.0",
"graphql": "^16.10.0",
"graphql-http": "^1.22.4",
"http-errors": "^2.0.0",
@@ -29739,7 +30205,7 @@
}
},
"packages/insomnia-testing": {
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"license": "Apache-2.0",
"dependencies": {
"chai-json-schema": "1.5.1"
diff --git a/packages/insomnia-api/package.json b/packages/insomnia-api/package.json
index 1f082aaf72e7..ea04bb53102b 100644
--- a/packages/insomnia-api/package.json
+++ b/packages/insomnia-api/package.json
@@ -2,7 +2,7 @@
"private": true,
"name": "insomnia-api",
"license": "Apache-2.0",
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"author": "Kong ",
"description": "Insomnia API functionalities",
"repository": {
diff --git a/packages/insomnia-inso/package.json b/packages/insomnia-inso/package.json
index 188e6cb5bcd3..542074579cee 100644
--- a/packages/insomnia-inso/package.json
+++ b/packages/insomnia-inso/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "insomnia-inso",
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"homepage": "https://insomnia.rest",
"description": "A CLI for Insomnia - The Collaborative API Design Tool",
"author": "Kong ",
diff --git a/packages/insomnia-scripting-environment/package.json b/packages/insomnia-scripting-environment/package.json
index 0ad8d06759b9..f8c0fc3bbca9 100644
--- a/packages/insomnia-scripting-environment/package.json
+++ b/packages/insomnia-scripting-environment/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "insomnia-scripting-environment",
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"description": "",
"main": "src/objects/index.ts",
"types": "src/objects/index.ts",
diff --git a/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/HEAD b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/HEAD
new file mode 100644
index 000000000000..cb089cd89a7d
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/config b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/config
new file mode 100644
index 000000000000..64280b806c97
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/config
@@ -0,0 +1,6 @@
+[core]
+ repositoryformatversion = 0
+ filemode = false
+ bare = true
+ symlinks = false
+ ignorecase = true
diff --git a/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/description b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/description
new file mode 100644
index 000000000000..498b267a8c78
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/description
@@ -0,0 +1 @@
+Unnamed repository; edit this file 'description' to name the repository.
diff --git a/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/info/exclude b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/info/exclude
new file mode 100644
index 000000000000..a5196d1be8fb
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/info/exclude
@@ -0,0 +1,6 @@
+# git ls-files --others --exclude-from=.git/info/exclude
+# Lines that start with '#' are comments.
+# For a project mostly in C, the following would be a good set of
+# exclude patterns (uncomment them if you want to use them):
+# *.[oa]
+# *~
diff --git a/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/objects/33/57649e085f0f653c60993fc34b0d380c4c2991 b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/objects/33/57649e085f0f653c60993fc34b0d380c4c2991
new file mode 100644
index 000000000000..78c4d86d1a52
Binary files /dev/null and b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/objects/33/57649e085f0f653c60993fc34b0d380c4c2991 differ
diff --git a/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/objects/5a/d28e22767f979da2c198dc6c1003b25964e3da b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/objects/5a/d28e22767f979da2c198dc6c1003b25964e3da
new file mode 100644
index 000000000000..a49384ff604c
Binary files /dev/null and b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/objects/5a/d28e22767f979da2c198dc6c1003b25964e3da differ
diff --git a/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/objects/73/a07b09e3e790de0277df5ac98e0ad62ecf83d4 b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/objects/73/a07b09e3e790de0277df5ac98e0ad62ecf83d4
new file mode 100644
index 000000000000..b6f9704d044c
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/objects/73/a07b09e3e790de0277df5ac98e0ad62ecf83d4
@@ -0,0 +1,3 @@
+xÎA
+ƒ0@Ñ®sŠì™dâ˜Rz•8«P‰‘âíëª'èö->_ò²ÌÕz—ZT-b×S`…Ø0R‡BÀŒ£`à$ˆgv&íuÊÅ)t^‹Ø+E&
+¾ùÑ}ß´lí;]_Gûœë´äåf]ß“G‡Ä¶`N=?ªþ¯h’ù…³D,
\ No newline at end of file
diff --git a/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/refs/heads/master b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/refs/heads/master
new file mode 100644
index 000000000000..1c22a987384c
--- /dev/null
+++ b/packages/insomnia-smoke-test/fixtures/git-repo/git-server.git/refs/heads/master
@@ -0,0 +1 @@
+73a07b09e3e790de0277df5ac98e0ad62ecf83d4
diff --git a/packages/insomnia-smoke-test/package.json b/packages/insomnia-smoke-test/package.json
index 303c65151468..2628cd6ff130 100644
--- a/packages/insomnia-smoke-test/package.json
+++ b/packages/insomnia-smoke-test/package.json
@@ -11,7 +11,7 @@
"bugs": {
"url": "https://github.com/kong/insomnia/issues"
},
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"scripts": {
"test:dev": "xvfb-maybe cross-env BUNDLE=dev playwright test",
"test:build": "xvfb-maybe cross-env BUNDLE=build playwright test",
@@ -30,6 +30,7 @@
"esbuild-runner": "2.2.2",
"express": "^4.21.2",
"express-basic-auth": "^1.2.1",
+ "git-http-mock-server": "^2.0.0",
"graphql": "^16.10.0",
"graphql-http": "^1.22.4",
"http-errors": "^2.0.0",
diff --git a/packages/insomnia-smoke-test/playwright.config.ts b/packages/insomnia-smoke-test/playwright.config.ts
index 36f49be05fcf..907d01ba00cf 100644
--- a/packages/insomnia-smoke-test/playwright.config.ts
+++ b/packages/insomnia-smoke-test/playwright.config.ts
@@ -1,4 +1,7 @@
+import os from 'node:os';
+
import type { PlaywrightTestConfig } from '@playwright/test';
+const isWindows = os.platform() === 'win32';
const config: PlaywrightTestConfig = {
projects: [
{
@@ -35,7 +38,7 @@ const config: PlaywrightTestConfig = {
},
},
reporter: process.env.CI ? [['github'], ['line']] : [['list']],
- timeout: process.env.CI ? 60 * 1000 : 20 * 1000,
+ timeout: process.env.CI || isWindows ? 60 * 1000 : 20 * 1000,
forbidOnly: !!process.env.CI,
outputDir: 'traces',
testDir: 'tests',
diff --git a/packages/insomnia-smoke-test/playwright/pages/preferences/credentials-tab.ts b/packages/insomnia-smoke-test/playwright/pages/preferences/credentials-tab.ts
new file mode 100644
index 000000000000..17f20b6d034b
--- /dev/null
+++ b/packages/insomnia-smoke-test/playwright/pages/preferences/credentials-tab.ts
@@ -0,0 +1,38 @@
+import type { ElectronApplication, Locator, Page } from '@playwright/test';
+
+import { BasePage } from '../base-page';
+
+/**
+ * Component for the **Credentials tab** within Insomnia Preferences.
+ *
+ * Handles credential management functionality:
+ * - Add, edit, and remove credentials
+ */
+export class PreferencesCredentialsTab extends BasePage {
+ constructor(
+ readonly page: Page,
+ readonly app: ElectronApplication,
+ ) {
+ super(page);
+ }
+
+ get root(): Locator {
+ return this.page.getByTestId('credentials-settings-tab');
+ }
+
+ async addAccessTokenGitCredential() {
+ await this.page.getByRole('button', { name: 'Create Git Credential' }).click();
+ await this.page.getByText('Access Token').click();
+ await this.page.getByRole('textbox', { name: 'Author Email' }).click();
+ await this.page.getByRole('textbox', { name: 'Author Email' }).fill('a@b.com');
+ await this.page.getByRole('textbox', { name: 'Author Name' }).click();
+ await this.page.getByRole('textbox', { name: 'Author Name' }).fill('author');
+ await this.page.getByRole('textbox', { name: 'Username' }).click();
+ await this.page.getByRole('textbox', { name: 'Username' }).fill('username');
+ await this.page.getByRole('textbox', { name: 'Git Access Token' }).click();
+ await this.page.getByRole('textbox', { name: 'Git Access Token' }).fill('accesstoken');
+ await this.page.getByRole('textbox', { name: 'Repository base URL' }).click();
+ await this.page.getByRole('textbox', { name: 'Repository base URL' }).fill('http://localhost:4010/git/');
+ await this.page.getByRole('button', { name: 'Save Credential' }).click();
+ }
+}
diff --git a/packages/insomnia-smoke-test/playwright/pages/preferences/index.ts b/packages/insomnia-smoke-test/playwright/pages/preferences/index.ts
index 995f2f5cfa0d..d58096b7adcf 100644
--- a/packages/insomnia-smoke-test/playwright/pages/preferences/index.ts
+++ b/packages/insomnia-smoke-test/playwright/pages/preferences/index.ts
@@ -1,25 +1,30 @@
import type { ElectronApplication, Locator, Page } from '@playwright/test';
+import { PreferencesCredentialsTab } from './credentials-tab';
import { PreferencesDataTab } from './data-tab';
-type PreferencesTab = 'Data' | 'General' | 'Themes' | 'Plugins' | 'Other';
+type PreferencesTab = 'Data' | 'General' | 'Themes' | 'Credentials' | 'Plugins' | 'Other';
/**
* Page Object for **Insomnia Preferences** modal.
*
* Composes preference tabs:
* - Data tab (import/export)
+ * - Credentials tab (Git credentials management)
* - Other tabs (themes, plugins, etc.) can be added as needed
*/
export class PreferencesPage {
/** Data tab (import/export functionality). */
readonly dataTab: PreferencesDataTab;
+ /** Credentials tab (Git credentials management). */
+ readonly credentialsTab: PreferencesCredentialsTab;
constructor(
readonly page: Page,
readonly app: ElectronApplication,
) {
this.dataTab = new PreferencesDataTab(page, app);
+ this.credentialsTab = new PreferencesCredentialsTab(page, app);
}
/** The root preferences dialog. */
@@ -43,7 +48,7 @@ export class PreferencesPage {
* Closes the preferences modal.
*/
async closePreferences(): Promise {
- await this.page.locator('.app').press('Escape');
+ await this.page.getByRole('button', { name: 'Modal Close Button' }).click();
await this.root.waitFor({ state: 'hidden' });
}
}
diff --git a/packages/insomnia-smoke-test/playwright/pages/project/index.ts b/packages/insomnia-smoke-test/playwright/pages/project/index.ts
index b174339a0e66..df1d6e81d55f 100644
--- a/packages/insomnia-smoke-test/playwright/pages/project/index.ts
+++ b/packages/insomnia-smoke-test/playwright/pages/project/index.ts
@@ -80,6 +80,33 @@ export class ProjectPage extends BasePage {
await this.page.getByRole('button', { name: 'Create', exact: true }).click();
}
+ async createGitSyncProject(name = 'My Git Project'): Promise {
+ await this.page.getByRole('button', { name: 'Create new Project' }).click();
+ await this.page.getByRole('textbox', { name: 'Project name' }).click();
+ await this.page.getByRole('textbox', { name: 'Project name' }).press('ControlOrMeta+a');
+ await this.page.getByRole('textbox', { name: 'Project name' }).fill(name);
+ await this.page.getByText('Git Sync').click();
+ await this.page.getByRole('button', { name: 'Access Token author Git' }).click();
+ await this.page.getByRole('option', { name: 'Custom Git Credential' }).click();
+ await this.page.getByRole('textbox', { name: 'Repository URL' }).click();
+ await this.page.getByRole('textbox', { name: 'Repository URL' }).fill('git-server.git');
+ await this.page.getByRole('button', { name: 'Show suggestions Branch' }).click();
+ await this.page.getByRole('option', { name: 'master' }).click();
+ await this.page.getByRole('button', { name: 'Scan for files' }).click();
+ await this.page.getByRole('button', { name: 'Create Blank Project' }).click();
+ const projectModalCloseButton = this.page.locator('[data-test-id="project-modal-close-button"]');
+ await projectModalCloseButton.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {});
+ if (await projectModalCloseButton.isVisible()) {
+ await projectModalCloseButton.click();
+ }
+ await this.page.getByRole('button', { name: 'Personal workspace' }).click();
+ await this.page.getByRole('option', { name: /Magic/ }).locator('span').click();
+ await this.page.getByRole('button', { name: /Magic/ }).click();
+ await this.page.getByRole('option', { name: 'Personal workspace' }).locator('span').click();
+ await this.page.getByText('Git Project').waitFor({ state: 'visible', timeout: 10_000 });
+ await this.page.getByText('Git Project').click();
+ }
+
// ===========================================================================
// Import Operations
// ===========================================================================
diff --git a/packages/insomnia-smoke-test/server/index.ts b/packages/insomnia-smoke-test/server/index.ts
index 0f29bad42b31..649160830d1b 100644
--- a/packages/insomnia-smoke-test/server/index.ts
+++ b/packages/insomnia-smoke-test/server/index.ts
@@ -6,6 +6,7 @@ import nodePath from 'node:path';
import * as bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import express from 'express';
+import gitMiddleware from 'git-http-mock-server/middleware';
import { createHandler } from 'graphql-http/lib/use/http';
import { basicAuthRouter } from './basic-auth';
@@ -152,6 +153,15 @@ app.get('/v1/oauth/azure/config', (_req, res) => {
});
});
+app.use(
+ '/git',
+ gitMiddleware({
+ root: nodePath.join(__dirname, '../fixtures/git-repo'),
+ glob: '*',
+ route: '/',
+ }),
+);
+
startWebSocketServer(
app.listen(port, '::', () => {
console.log(`Listening at http://localhost:${port}`);
diff --git a/packages/insomnia-smoke-test/tests/smoke/disable-git-sync.test.ts b/packages/insomnia-smoke-test/tests/smoke/disable-git-sync.test.ts
new file mode 100644
index 000000000000..4f476cbe3196
--- /dev/null
+++ b/packages/insomnia-smoke-test/tests/smoke/disable-git-sync.test.ts
@@ -0,0 +1,101 @@
+import { expect } from '@playwright/test';
+
+import { test } from '../../playwright/test';
+
+const mockCredentials = {
+ email: 'insomnia-test@konghq.com',
+ gitUsername: 'insomnia-test',
+ username: 'insomnia',
+ token: '12345',
+ baseUrl: 'https://fakeurl.com/',
+};
+
+test.describe('Git Sync', () => {
+ test.describe('with git sync feature flag disabled', () => {
+ test.beforeEach(async ({ request }) => {
+ // Disable git sync feature flag for organization
+ await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/features', {
+ data: {
+ features: {
+ gitSync: {
+ enabled: false,
+ },
+ konnectSync: {
+ enabled: true,
+ },
+ },
+ },
+ });
+ });
+
+ test.afterEach(async ({ request }) => {
+ // Re-enable git sync feature flag for organization
+ await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/features', {
+ data: {
+ features: {
+ gitSync: {
+ enabled: true,
+ },
+ konnectSync: {
+ enabled: true,
+ },
+ },
+ },
+ });
+ });
+
+ test('should disable git sync usage', async ({ page }) => {
+ await page.getByTestId('settings-button').click();
+ await page.getByRole('tab', { name: 'Credentials' }).click();
+ await page.getByRole('button', { name: 'Create Git Credential' }).click();
+ await page.getByText('Access Token').click();
+ await page.getByRole('textbox', { name: 'Author Email' }).fill(mockCredentials.email);
+ await page.getByRole('textbox', { name: 'Author Name' }).fill(mockCredentials.gitUsername);
+ await page.getByRole('textbox', { name: 'Username', exact: true }).fill(mockCredentials.username);
+ await page.getByRole('textbox', { name: 'Git Access Token' }).fill(mockCredentials.token);
+ await page.getByRole('textbox', { name: 'Repository base URL' }).fill(mockCredentials.baseUrl);
+ await page.getByRole('button', { name: 'Save Credential' }).click();
+ await page.getByRole('button', { name: 'Modal Close Button' }).click();
+ await page.getByRole('button', { name: 'Create new Project' }).click();
+ await page.getByLabel('Project Type Item: git').click();
+ await expect.soft(page.getByLabel('Git Sync Feature Disabled Banner')).toBeVisible();
+
+ await expect.soft(page.getByLabel('Git Setup Form')).toBeHidden();
+ await expect.soft(page.getByRole('button', { name: 'Scan for files' })).toBeDisabled();
+ });
+ });
+
+ test.describe('with git storage rule disabled', () => {
+ test.beforeEach(async ({ request }) => {
+ // Set storage rule to disable git sync
+ await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/storage-rule', {
+ data: {
+ enableCloudSync: true,
+ enableGitSync: false,
+ enableLocalVault: true,
+ isOverridden: false,
+ },
+ });
+ });
+
+ test.afterEach(async ({ request }) => {
+ // reset the storage rule after test
+ await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/storage-rule', {
+ data: {
+ enableCloudSync: true,
+ enableGitSync: true,
+ enableLocalVault: true,
+ isOverridden: false,
+ },
+ });
+ });
+
+ test('disable git sync selection', async ({ page }) => {
+ await page.getByRole('button', { name: 'Create new Project' }).click();
+ const banner = page.getByLabel('Project Storage Restriction Banner');
+ await expect.soft(banner).toBeVisible();
+ await expect.soft(banner).not.toHaveText('Git Sync');
+ await expect.soft(page.getByLabel('Project Type: git')).toBeDisabled();
+ });
+ });
+});
diff --git a/packages/insomnia-smoke-test/tests/smoke/export.test.ts b/packages/insomnia-smoke-test/tests/smoke/export.test.ts
index fb0cd9f4d9c8..563801382ddb 100644
--- a/packages/insomnia-smoke-test/tests/smoke/export.test.ts
+++ b/packages/insomnia-smoke-test/tests/smoke/export.test.ts
@@ -33,9 +33,8 @@ test.describe('Export', () => {
await insomnia.preferencesPage.switchToPreferenceTab('Data');
await insomnia.preferencesPage.dataTab.exportProjectData(tempDir, 'yaml');
await waitForExportFiles(tempDir, 2);
- await insomnia.preferencesPage.closePreferences();
const exportedFiles = getExportedFiles(tempDir);
- expect.soft(exportedFiles.length).toBe(2);
+ expect.soft(exportedFiles).toHaveLength(2);
const fixtureMap: Record = {
'Collection-A': FIXTURE_FILES[0],
'Collection-B': FIXTURE_FILES[1],
@@ -78,7 +77,7 @@ test.describe('Export', () => {
await insomnia.preferencesPage.dataTab.exportAllData(tempDir);
await insomnia.preferencesPage.closePreferences();
const exportedFiles = getExportedFiles(tempDir).filter((file: string) => !file.includes('scratchpad'));
- expect.soft(exportedFiles.length).toBe(2);
+ expect.soft(exportedFiles).toHaveLength(2);
const fixtureMap: Record = {
'Collection-A': FIXTURE_FILES[0],
'Collection-B': FIXTURE_FILES[1],
@@ -123,8 +122,6 @@ test.describe('Export', () => {
await insomnia.preferencesPage.dataTab.exportProjectData(exportFilePath, 'har');
await waitForExportFiles(tempDir, 1);
- await insomnia.preferencesPage.closePreferences();
-
const exportedContent = readExportedFile(exportFilePath);
const har = JSON.parse(exportedContent);
diff --git a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts
index 4f476cbe3196..c604c4ab8dbe 100644
--- a/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts
+++ b/packages/insomnia-smoke-test/tests/smoke/git-sync.test.ts
@@ -1,101 +1,96 @@
import { expect } from '@playwright/test';
+import type { InsomniaApp } from '../../playwright/pages';
import { test } from '../../playwright/test';
-const mockCredentials = {
- email: 'insomnia-test@konghq.com',
- gitUsername: 'insomnia-test',
- username: 'insomnia',
- token: '12345',
- baseUrl: 'https://fakeurl.com/',
-};
-
test.describe('Git Sync', () => {
- test.describe('with git sync feature flag disabled', () => {
- test.beforeEach(async ({ request }) => {
- // Disable git sync feature flag for organization
- await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/features', {
- data: {
- features: {
- gitSync: {
- enabled: false,
- },
- konnectSync: {
- enabled: true,
- },
- },
- },
- });
- });
-
- test.afterEach(async ({ request }) => {
- // Re-enable git sync feature flag for organization
- await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/features', {
- data: {
- features: {
- gitSync: {
- enabled: true,
- },
- konnectSync: {
- enabled: true,
- },
- },
- },
- });
- });
+ test.slow();
- test('should disable git sync usage', async ({ page }) => {
- await page.getByTestId('settings-button').click();
- await page.getByRole('tab', { name: 'Credentials' }).click();
- await page.getByRole('button', { name: 'Create Git Credential' }).click();
- await page.getByText('Access Token').click();
- await page.getByRole('textbox', { name: 'Author Email' }).fill(mockCredentials.email);
- await page.getByRole('textbox', { name: 'Author Name' }).fill(mockCredentials.gitUsername);
- await page.getByRole('textbox', { name: 'Username', exact: true }).fill(mockCredentials.username);
- await page.getByRole('textbox', { name: 'Git Access Token' }).fill(mockCredentials.token);
- await page.getByRole('textbox', { name: 'Repository base URL' }).fill(mockCredentials.baseUrl);
- await page.getByRole('button', { name: 'Save Credential' }).click();
- await page.getByRole('button', { name: 'Modal Close Button' }).click();
- await page.getByRole('button', { name: 'Create new Project' }).click();
- await page.getByLabel('Project Type Item: git').click();
- await expect.soft(page.getByLabel('Git Sync Feature Disabled Banner')).toBeVisible();
+ test('Create new branch and switch to it', async ({ insomnia, page }) => {
+ await addAccessTokenGitCredential(insomnia);
+ await insomnia.projectPage.createGitSyncProject();
- await expect.soft(page.getByLabel('Git Setup Form')).toBeHidden();
- await expect.soft(page.getByRole('button', { name: 'Scan for files' })).toBeDisabled();
- });
+ await page.getByTestId('git-dropdown').click();
+ await page.getByRole('menuitemradio', { name: 'Branches' }).click();
+ await page.getByRole('textbox', { name: 'New branch name:' }).click();
+ await page.getByRole('textbox', { name: 'New branch name:' }).fill('branch1');
+ await page.getByRole('button', { name: 'Create', exact: true }).click();
+ await expect.soft(page.getByText('branch1 *')).toBeVisible();
});
- test.describe('with git storage rule disabled', () => {
- test.beforeEach(async ({ request }) => {
- // Set storage rule to disable git sync
- await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/storage-rule', {
- data: {
- enableCloudSync: true,
- enableGitSync: false,
- enableLocalVault: true,
- isOverridden: false,
- },
- });
- });
+ test('Commit and check history', async ({ insomnia, page }) => {
+ await addAccessTokenGitCredential(insomnia);
+ await insomnia.projectPage.createGitSyncProject();
+
+ await page.getByRole('button', { name: 'New request collection' }).click();
+ await page.getByRole('textbox', { name: 'Name', exact: true }).click();
+ await page.getByRole('textbox', { name: 'Name', exact: true }).press('ControlOrMeta+a');
+ await page.getByRole('textbox', { name: 'Name', exact: true }).fill('Collection 1');
+ await page.getByRole('textbox', { name: 'File name my_collection' }).click();
+ await page.getByRole('textbox', { name: 'File name my_collection' }).press('ControlOrMeta+a');
+ await page.getByRole('textbox', { name: 'File name my_collection' }).fill('collection_1');
+ await page.getByRole('button', { name: 'Create', exact: true }).click();
+ await page.getByTestId('git-dropdown').click();
+ await expect.soft(page.getByRole('menuitemradio', { name: 'Commit' })).toBeVisible();
+ await page.getByRole('menuitemradio', { name: 'Commit' }).click();
+ await expect.soft(page.getByLabel('Unstaged changes').locator('span')).toContainText('collection_1.yaml');
- test.afterEach(async ({ request }) => {
- // reset the storage rule after test
- await request.post('http://127.0.0.1:4010/v1/test-utils/organizations/storage-rule', {
- data: {
- enableCloudSync: true,
- enableGitSync: true,
- enableLocalVault: true,
- isOverridden: false,
- },
- });
- });
+ await page.locator('button[name="Stage all changes"]').click();
+ await page.getByRole('textbox', { name: 'Message' }).click();
+ await page.getByRole('textbox', { name: 'Message' }).fill('1');
+ await page.getByRole('button', { name: 'Commit', exact: true }).click();
+ await page.getByTestId('git-dropdown').click();
+ await page.getByText('History').click();
+ await expect.soft(page.getByLabel('1', { exact: true }).getByRole('rowheader')).toContainText('1');
+ });
+
+ test('Merge branch and verify changes on the other branch has been merged into current branch', async ({
+ insomnia,
+ page,
+ }) => {
+ await addAccessTokenGitCredential(insomnia);
+ await insomnia.projectPage.createGitSyncProject();
- test('disable git sync selection', async ({ page }) => {
- await page.getByRole('button', { name: 'Create new Project' }).click();
- const banner = page.getByLabel('Project Storage Restriction Banner');
- await expect.soft(banner).toBeVisible();
- await expect.soft(banner).not.toHaveText('Git Sync');
- await expect.soft(page.getByLabel('Project Type: git')).toBeDisabled();
- });
+ await page.getByTestId('git-dropdown').click();
+ await page.getByRole('menuitemradio', { name: 'Branches' }).click();
+ await page.getByRole('textbox', { name: 'New branch name:' }).click();
+ await page.getByRole('textbox', { name: 'New branch name:' }).fill('branch1');
+ await page.getByRole('button', { name: 'Create', exact: true }).click();
+ await expect.soft(page.getByText('branch1 *')).toBeVisible();
+ await page.getByTestId('close-git-project-branches-modal').click();
+ await page.getByTestId('git-project-branches-modal-overlay').waitFor({ state: 'hidden' });
+ await page.getByRole('button', { name: 'New request collection' }).click();
+ await page.getByRole('textbox', { name: 'Name', exact: true }).click();
+ await page.getByRole('textbox', { name: 'Name', exact: true }).press('ControlOrMeta+a');
+ await page.getByRole('textbox', { name: 'Name', exact: true }).fill('collection 1');
+ await page.getByRole('textbox', { name: 'File name my_collection' }).click();
+ await page.getByRole('textbox', { name: 'File name my_collection' }).press('ControlOrMeta+a');
+ await page.getByRole('textbox', { name: 'File name my_collection' }).fill('collection_1');
+ await page.getByRole('button', { name: 'Create', exact: true }).click();
+ await page.getByTestId('project').click();
+ await page.getByTestId('git-dropdown').click();
+ await page.getByRole('menuitemradio', { name: 'Commit' }).click();
+ await page.locator('button[name="Stage all changes"]').click();
+ await page.getByRole('textbox', { name: 'Message' }).click();
+ await page.getByRole('textbox', { name: 'Message' }).fill('commit 1');
+ await page.getByRole('button', { name: 'Commit', exact: true }).click();
+ await page.getByTestId('git-dropdown').click();
+ await page.getByRole('menuitemradio', { name: 'master' }).click();
+ await page.locator('html').click();
+ await page.getByTestId('git-dropdown').click();
+ await page.getByRole('menuitemradio', { name: 'Branches' }).click();
+ await page.getByLabel('branch1').getByRole('button', { name: 'Merge' }).click();
+ await page.getByRole('button', { name: 'ïª Confirm' }).click();
+ await page.getByTestId('close-git-project-branches-modal').click();
+ await page.getByTestId('git-project-branches-modal-overlay').waitFor({ state: 'hidden' });
+ await expect.soft(page.getByText('collection 1')).toBeVisible();
});
});
+
+async function addAccessTokenGitCredential(insomnia: InsomniaApp) {
+ await insomnia.statusbar.openPreferences();
+ await insomnia.preferencesPage.switchToPreferenceTab('Credentials');
+ await insomnia.preferencesPage.credentialsTab.addAccessTokenGitCredential();
+ await expect.soft(insomnia.page.getByRole('row', { name: 'Custom Git Credential' })).toBeVisible();
+ await insomnia.preferencesPage.closePreferences();
+}
diff --git a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts
index 2f7a958f6972..582ae61c69d0 100644
--- a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts
+++ b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-features.test.ts
@@ -208,6 +208,7 @@ test.describe('pre-request features tests', () => {
}),
};
});
+
test('run test cases', async ({ page }) => {
for (const tc of testCases) {
console.log(`Running test case: ${tc.name}`);
@@ -230,6 +231,7 @@ test.describe('pre-request features tests', () => {
tc.customVerify(bodyJson);
}
});
+
test('send request with content type', async ({ page }) => {
await page.getByTestId('settings-button').click();
await page.getByTestId('dataFolders').click();
@@ -660,14 +662,17 @@ test.describe('unhappy paths', () => {
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
// verify
- await expect.soft(page.getByTestId('response-pane')).toContainText('my custom error');
+ await expect
+ .soft(page.getByTestId('response-pane'))
+ .toContainText(`my custom error`);
await page.getByRole('tab', { name: 'Scripts' }).click();
await page.getByTestId('CodeEditor').getByRole('textbox').press('ControlOrMeta+a');
await page.keyboard.press('Backspace');
await editor.fill(`insomnia.INVALID_FIELD.set('', '')`);
- await page.getByRole('tab', { name: 'Body' }).click();
+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
+ await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
// send
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
@@ -678,3 +683,174 @@ test.describe('unhappy paths', () => {
.toContainText(`Cannot read properties of undefined (reading 'set')`);
});
});
+
+test.describe('sandbox features', () => {
+ test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
+
+ test.beforeEach(async ({ app, page }) => {
+ const text = await loadFixture('pre-request-collection.yaml');
+ await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
+
+ await page.getByLabel('Import').click();
+ await page.locator('[data-test-id="import-from-clipboard"]').click();
+ await page.getByRole('button', { name: 'Scan' }).click();
+ await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click();
+ });
+
+ // Blocked Roots / Scopes group: 'this' is blocked.
+ test('blocked roots / scopes group', async ({ page }) => {
+ await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
+
+
+ await page.getByRole('tab', { name: 'Scripts' }).click();
+ const editor = page.getByTestId('CodeEditor').getByRole('textbox');
+
+ // enter script that accesses a property on 'this'.
+ await editor.fill(`insomnia.environment.set('result', String(this?.process));`);
+
+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
+ await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
+
+ await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
+
+ // verify blocked-root error
+ await expect
+ .soft(page.getByTestId('response-pane'))
+ .toContainText("The script was blocked because it used 'this'.");
+
+ // navigate to Settings → Scripting, disable the "Scopes" blocked roots group
+ await page.getByTestId('settings-button').click();
+ await page.locator('text=Insomnia Preferences').first().click();
+ await page.getByRole('tab', { name: 'Scripting' }).click();
+ const scopesSwitch = page.locator('div:has(> h4:has-text("Scopes")) label[data-react-aria-pressable]');
+ await scopesSwitch.scrollIntoViewIfNeeded();
+ await scopesSwitch.click();
+
+ await page.locator('.app').press('Escape');
+
+ // re-send — no sandbox error; this === undefined.
+ await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
+ await expect
+ .soft(page.getByTestId('response-pane'))
+ .not.toContainText("The script was blocked because it used 'this'.");
+ await expect
+ .soft(page.locator('[data-testid="response-status-tag"]:visible'))
+ .toContainText('200 OK');
+ });
+
+ // Blocked Properties / Prototype Mutation group: 'prototype' is blocked.
+ test('blocked properties / prototype mutation group', async ({ page }) => {
+ await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
+
+ // enter script that accesses Object.prototype.
+ await page.getByRole('tab', { name: 'Scripts' }).click();
+ const editor = page.getByTestId('CodeEditor').getByRole('textbox');
+ await editor.fill(`insomnia.environment.set('result', typeof Object.prototype.toString);`);
+
+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
+ await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
+
+ await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
+
+ // verify blocked-property error
+ await expect
+ .soft(page.getByTestId('response-pane'))
+ .toContainText("The script was blocked because it used the property 'prototype'.");
+
+ // navigate to Settings → Scripting, disable the "Prototype Mutation" blocked properties group
+ await page.getByTestId('settings-button').click();
+ await page.locator('text=Insomnia Preferences').first().click();
+ await page.getByRole('tab', { name: 'Scripting' }).click();
+ const protoMutationSwitch = page.locator('div:has(> h4:has-text("Prototype Mutation")) label[data-react-aria-pressable]');
+ await protoMutationSwitch.scrollIntoViewIfNeeded();
+ await protoMutationSwitch.click();
+ await expect.soft(protoMutationSwitch).not.toHaveAttribute('data-selected');
+ await page.locator('.app').press('Escape');
+
+ // re-send — prototype access now allowed; Object.prototype.toString is a function
+ await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
+ await expect
+ .soft(page.getByTestId('response-pane'))
+ .not.toContainText("The script was blocked because it used the property 'prototype'.");
+ await expect
+ .soft(page.locator('[data-testid="response-status-tag"]:visible'))
+ .toContainText('200 OK');
+ });
+
+ // Mask Rules / Runtime APIs group: 'Function' is masked to undefined at runtime.
+ test('Mask Rules / Runtime APIs group.', async ({ page }) => {
+ await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
+
+ // enter script that uses the Function constructor, only masked at runtime.
+ await page.getByRole('tab', { name: 'Scripts' }).click();
+ const editor = page.getByTestId('CodeEditor').getByRole('textbox');
+ await editor.fill(`const f = new Function('return 42'); insomnia.environment.set('result', f());`);
+
+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
+ await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
+
+ // send — Function masked to undefined → V8 uses the identifier name: "Function is not a constructor"
+ await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
+
+ await expect
+ .soft(page.getByTestId('response-pane'))
+ .toContainText('Function is not a constructor');
+
+ // navigate to Settings → Scripting, disable the "Runtime APIs" mask group
+ await page.getByTestId('settings-button').click();
+ await page.locator('text=Insomnia Preferences').first().click();
+ await page.getByRole('tab', { name: 'Scripting' }).click();
+ const runtimeApisSwitch = page.locator('div:has(> h4:has-text("Runtime APIs")) label[data-react-aria-pressable]');
+ await runtimeApisSwitch.scrollIntoViewIfNeeded();
+ await runtimeApisSwitch.click();
+ await expect.soft(runtimeApisSwitch).not.toHaveAttribute('data-selected');
+ await page.locator('.app').press('Escape');
+
+ // re-send — Function is now the real constructor; script returns 42
+ await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
+ await expect
+ .soft(page.getByTestId('response-pane'))
+ .not.toContainText('Function is not a constructor');
+ await expect
+ .soft(page.locator('[data-testid="response-status-tag"]:visible'))
+ .toContainText('200 OK');
+ });
+
+ test('Layered security / unblocked properties resolve undefined', async ({ page }) => {
+ await page.getByLabel('Request Collection').getByTestId('echo pre-request script result').press('Enter');
+
+ // enter script that accesses a property on 'process'.
+ await page.getByRole('tab', { name: 'Scripts' }).click();
+ const editor = page.getByTestId('CodeEditor').getByRole('textbox');
+ await editor.fill(`insomnia.environment.set('result', String(process?.version));`);
+
+ // CodeMirror debounces onChange by DEBOUNCE_MILLIS (100ms).
+ await page.evaluate(() => new Promise(resolve => setTimeout(resolve, 150)));
+
+ await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
+
+ // verify blocked-root error
+ await expect
+ .soft(page.getByTestId('response-pane'))
+ .toContainText("The script was blocked because it used 'process'.");
+
+ // navigate to Settings → Scripting, disable only the "Node.js Internals" BLOCKED ROOTS group.
+ await page.getByTestId('settings-button').click();
+ await page.locator('text=Insomnia Preferences').first().click();
+ await page.getByRole('tab', { name: 'Scripting' }).click();
+ const nodeInternalsSwitch = page.locator('xpath=//h4[normalize-space(text())="Node.js Internals"]/following-sibling::div[1]//label[@data-react-aria-pressable]');
+ await nodeInternalsSwitch.scrollIntoViewIfNeeded();
+ await nodeInternalsSwitch.click();
+ await expect.soft(nodeInternalsSwitch).not.toHaveAttribute('data-selected');
+ await page.locator('.app').press('Escape');
+
+ // process?.version === undefined.
+ await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
+ await expect
+ .soft(page.getByTestId('response-pane'))
+ .not.toContainText("The script was blocked because it used 'process'.");
+ await expect
+ .soft(page.locator('[data-testid="response-status-tag"]:visible'))
+ .toContainText('200 OK');
+ });
+});
diff --git a/packages/insomnia-testing/package.json b/packages/insomnia-testing/package.json
index d767e046ef59..36461aa0f1c2 100644
--- a/packages/insomnia-testing/package.json
+++ b/packages/insomnia-testing/package.json
@@ -2,7 +2,7 @@
"private": true,
"name": "insomnia-testing",
"license": "Apache-2.0",
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"author": "Kong ",
"repository": {
"type": "git",
diff --git a/packages/insomnia/config/renderer-node-import-baseline.json b/packages/insomnia/config/renderer-node-import-baseline.json
index 935a32e7648a..5dfb9700f07e 100644
--- a/packages/insomnia/config/renderer-node-import-baseline.json
+++ b/packages/insomnia/config/renderer-node-import-baseline.json
@@ -92,6 +92,18 @@
"importer": "src/script-executor.ts",
"builtin": "fs/promises"
},
+ {
+ "importer": "src/scripting/require-interceptor.ts",
+ "builtin": "buffer"
+ },
+ {
+ "importer": "src/scripting/require-interceptor.ts",
+ "builtin": "timers"
+ },
+ {
+ "importer": "src/scripting/require-interceptor.ts",
+ "builtin": "util"
+ },
{
"importer": "src/templating/base-extension.ts",
"builtin": "crypto"
diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json
index 269a765447d1..9ecebaf9e9b2 100644
--- a/packages/insomnia/package.json
+++ b/packages/insomnia/package.json
@@ -1,6 +1,6 @@
{
"name": "insomnia",
- "version": "12.5.1-alpha.0",
+ "version": "12.6.0-beta.0",
"productName": "Insomnia",
"private": true,
"description": "The Collaborative API Design Tool",
@@ -73,6 +73,8 @@
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-virtual": "3.13.12",
"@xmldom/xmldom": "^0.9.8",
+ "acorn": "^8.16.0",
+ "acorn-walk": "^8.3.5",
"ajv": "^8.17.1",
"apiconnect-wsdl": "2.0.36",
"aws4": "^1.13.2",
diff --git a/packages/insomnia/src/account/session.ts b/packages/insomnia/src/account/session.ts
index 838ccf5c5294..3b9a7f9c585b 100644
--- a/packages/insomnia/src/account/session.ts
+++ b/packages/insomnia/src/account/session.ts
@@ -225,7 +225,8 @@ async function _removeAllCredentials() {
*
*/
async function _removeGitRepository(repo: GitRepository) {
- const projects = await database.find(models.project.type, { gitRepositoryId: repo._id });
+ const queryIds = models.project.getQueryableGitRepositoryIds(repo._id);
+ const projects = await database.find(models.project.type, { gitRepositoryId: { $in: queryIds } });
for (const p of projects) {
await services.project.update(p, { gitRepositoryId: models.project.EMPTY_GIT_PROJECT_ID });
}
diff --git a/packages/insomnia/src/common/settings.ts b/packages/insomnia/src/common/settings.ts
index 0f8c716c471b..6ded268f7016 100644
--- a/packages/insomnia/src/common/settings.ts
+++ b/packages/insomnia/src/common/settings.ts
@@ -163,6 +163,16 @@ export interface Settings {
saveVaultKeyToOSSecretManager: boolean;
vaultSecretCacheDuration: number;
dataFolders: string[];
+ // AST and shadowing check.
+ scriptSandboxEnabled: boolean;
+ // Wraps the user script in 'use strict', preventing accidental globals and making `this` undefined.
+ scriptStrictModeEnabled: boolean;
+ // Names of security rules that have been individually disabled.
+ disabledSecurityRules: string[];
+ // AST blocked-property names that have been individually disabled.
+ disabledBlockedProperties: string[];
+ // AST blocked-root names that have been individually disabled.
+ disabledBlockedRoots: string[];
/** Custom npm registry URL for plugin installation (e.g., corporate mirror). Empty string uses the default https://registry.npmjs.org/. */
npmRegistryUrl: string;
}
diff --git a/packages/insomnia/src/entry.hidden-window-preload.ts b/packages/insomnia/src/entry.hidden-window-preload.ts
index 49aa39f3cbc0..b0ff3265673a 100644
--- a/packages/insomnia/src/entry.hidden-window-preload.ts
+++ b/packages/insomnia/src/entry.hidden-window-preload.ts
@@ -13,7 +13,7 @@ import {
stopMonitorAsyncTasks,
} from '../../insomnia-scripting-environment/src/objects';
// this will also import lots of node_modules into the preload script, consider moving this file insomnia-scripting-environment
-import { requireInterceptor } from './require-interceptor';
+import { requireInterceptor } from './scripting/require-interceptor';
export interface HiddenBrowserWindowToMainBridgeAPI {
requireInterceptor: (module: string) => any;
diff --git a/packages/insomnia/src/entry.hidden-window.ts b/packages/insomnia/src/entry.hidden-window.ts
index 08521a8e3fad..64a305301edb 100644
--- a/packages/insomnia/src/entry.hidden-window.ts
+++ b/packages/insomnia/src/entry.hidden-window.ts
@@ -1,23 +1,12 @@
import * as Sentry from '@sentry/electron/renderer';
-import * as _ from 'es-toolkit/compat';
import { SENTRY_OPTIONS } from 'insomnia/src/common/sentry';
-import {
- initInsomniaObject,
- InsomniaObject,
- waitForAllTestsDone,
-} from '../../insomnia-scripting-environment/src/objects';
-import {
- getNewConsole,
- mergeClientCertificates,
- mergeCookieJar,
- mergeRequests,
- mergeSettings,
- type RequestContext,
-} from '../../insomnia-scripting-environment/src/objects';
+import type { RequestContext } from '../../insomnia-scripting-environment/src/objects';
+import { runScript } from './scripting/run-script';
+import { type ScriptSecurityPolicy } from './scripting/sandbox';
export interface HiddenBrowserWindowBridgeAPI {
- runScript: (options: { script: string; context: RequestContext }) => Promise;
+ runScript: (options: { script: string; context: RequestContext; securityPolicy?: ScriptSecurityPolicy }) => Promise;
}
Sentry.init({
@@ -38,14 +27,17 @@ window.bridge.onmessage(
const result = await window.bridge.Promise.race([timeoutPromise, runScript(data)]);
callback(result);
} catch (err) {
- const errMessage = err.message
- ? `Error from Pre-request or after-response script:
-
-${err.message}`
- : err;
- const fullErrMessage = `${errMessage}
-
-${err.stack ? `Stack: ${err.stack}` : ''}`;
+ const error = err instanceof Error ? err : new Error(String(err));
+ if ((error as NodeJS.ErrnoException).code === 'SECURITY_POLICY_VIOLATION') {
+ console.log('[hidden-window] security policy violation:', error.message);
+ callback({ error: error.message });
+ return;
+ }
+ const errMessage = error.message
+ ? `Error from Pre-request or after-response script:\n${error.message}`
+ : String(error);
+ const fullErrMessage = `${errMessage}\n\n${error.stack ? `Stack: ${error.stack}` : ''}`;
+ console.log('[hidden-window] script error:', errMessage);
Sentry.captureException(errMessage, {
tags: {
source: 'hidden-window',
@@ -57,112 +49,3 @@ ${err.stack ? `Stack: ${err.stack}` : ''}`;
}
},
);
-
-// This function is duplicated in scriptExecutor.ts to run in nodejs
-// TODO: consider removing this implementation and using only nodejs scripting
-const runScript = async ({ script, context }: { script: string; context: RequestContext }): Promise => {
- const scriptConsole = getNewConsole();
-
- const executionContext = await initInsomniaObject(context, scriptConsole.log);
-
- const AsyncFunction = (async () => {}).constructor;
- const executeScript = AsyncFunction(
- 'insomnia',
- 'require',
- 'console',
- '_',
- 'setTimeout',
- // disable these as they are not supported in web or existing implementation
- 'setImmediate',
- 'queueMicrotask',
- 'process',
- 'waitForAllTestsDone',
- `
- const $ = insomnia;
- window.bridge.resetAsyncTasks(); // exclude unnecessary ones
- ${script};
- await waitForAllTestsDone();
- window.bridge.stopMonitorAsyncTasks(); // the next one should not be monitored
- await window.bridge.asyncTasksAllSettled();
- return insomnia;`,
- );
-
- const mutatedInsomniaObject = await executeScript(
- executionContext,
- window.bridge.requireInterceptor,
- scriptConsole,
- _,
- proxiedSetTimeout,
- undefined,
- undefined,
- undefined,
- waitForAllTestsDone,
- );
- if (mutatedInsomniaObject == null || !(mutatedInsomniaObject instanceof InsomniaObject)) {
- throw new Error('insomnia object is invalid or script returns earlier than expected.');
- }
- const mutatedContextObject = mutatedInsomniaObject.toObject();
- const updatedRequest = mergeRequests(context.request, mutatedContextObject.request);
- const updatedSettings = mergeSettings(context.settings, mutatedContextObject.request);
- const updatedCertificates = mergeClientCertificates(
- mutatedContextObject.clientCertificates,
- mutatedContextObject.request,
- );
- const updatedCookieJar = mergeCookieJar(context.cookieJar, mutatedContextObject.cookieJar);
-
- return {
- ...context,
- environment: {
- id: context.environment.id,
- name: context.environment.name,
- data: mutatedContextObject.environment,
- },
- baseEnvironment: {
- id: context.baseEnvironment.id,
- name: context.baseEnvironment.name,
- data: mutatedContextObject.baseEnvironment,
- },
- iterationData: context.iterationData
- ? {
- name: context.iterationData.name,
- data: mutatedContextObject.iterationData,
- }
- : undefined,
- transientVariables: {
- name: context.transientVariables?.name || 'transientVariables',
- data: mutatedContextObject.variables,
- },
- request: updatedRequest,
- execution: mutatedContextObject.execution,
- settings: updatedSettings,
- clientCertificates: updatedCertificates,
- cookieJar: updatedCookieJar,
- globals: context.globals && {
- id: context.globals.id,
- name: context.globals.name,
- data: mutatedContextObject.globals,
- },
- baseGlobals: context.baseGlobals && {
- id: context.baseGlobals.id,
- name: context.baseGlobals.name,
- data: mutatedContextObject.baseGlobals,
- },
- requestTestResults: mutatedContextObject.requestTestResults,
- logs: scriptConsole.dumpLogsAsArray(),
- parentFolders: mutatedContextObject.parentFolders,
- };
-};
-
-// proxiedSetTimeout has to be here as callback could be an async task
-function proxiedSetTimeout(callback: () => void, ms?: number | undefined) {
- let resolveHdl: (value: unknown) => void;
-
- new Promise(resolve => {
- resolveHdl = resolve;
- });
-
- return setTimeout(() => {
- callback();
- resolveHdl(null);
- }, ms);
-}
diff --git a/packages/insomnia/src/entry.preload.ts b/packages/insomnia/src/entry.preload.ts
index af3298e0a260..a4319e6c75e0 100644
--- a/packages/insomnia/src/entry.preload.ts
+++ b/packages/insomnia/src/entry.preload.ts
@@ -196,7 +196,7 @@ const git: GitServiceAPI = {
pullFromGitRemote: options => invokeWithNormalizedError('git.pullFromGitRemote', options),
continueMerge: options => invokeWithNormalizedError('git.continueMerge', options),
discardChanges: options => invokeWithNormalizedError('git.discardChanges', options),
- abortMerge: () => invokeWithNormalizedError('git.abortMerge'),
+ abortMerge: options => invokeWithNormalizedError('git.abortMerge', options),
gitStatus: options => invokeWithNormalizedError('git.gitStatus', options),
diff: () => invokeWithNormalizedError('git.diff'),
multipleCommitToGitRepo: options => invokeWithNormalizedError('git.multipleCommitToGitRepo', options),
@@ -265,7 +265,8 @@ const main: Window['main'] = {
requestId: string,
authentication: AuthTypeOAuth2,
forceRefresh?: boolean,
- ): Promise => invokeWithNormalizedError('getOAuth2Token', requestId, authentication, forceRefresh),
+ ): Promise =>
+ invokeWithNormalizedError('getOAuth2Token', requestId, authentication, forceRefresh),
insecureReadFile: options => invokeWithNormalizedError('insecureReadFile', options),
insecureReadFileWithEncoding: options => invokeWithNormalizedError('insecureReadFileWithEncoding', options),
secureReadFile: options => invokeWithNormalizedError('secureReadFile', options),
diff --git a/packages/insomnia/src/insomnia-data/node-src/services/project.ts b/packages/insomnia/src/insomnia-data/node-src/services/project.ts
index 8deb40117570..41d57097ceb2 100644
--- a/packages/insomnia/src/insomnia-data/node-src/services/project.ts
+++ b/packages/insomnia/src/insomnia-data/node-src/services/project.ts
@@ -16,8 +16,9 @@ export function getByRemoteId(remoteId: string) {
}
export function getAllByGitRepositoryIds(gitRepositoryIds: string[]) {
+ const queryIds = gitRepositoryIds.flatMap(id => models.project.getQueryableGitRepositoryIds(id));
return db.find(type, {
- gitRepositoryId: { $in: gitRepositoryIds },
+ gitRepositoryId: { $in: queryIds },
});
}
diff --git a/packages/insomnia/src/insomnia-data/src/models/project.ts b/packages/insomnia/src/insomnia-data/src/models/project.ts
index b5692ca73f0f..3ca8ada262d8 100644
--- a/packages/insomnia/src/insomnia-data/src/models/project.ts
+++ b/packages/insomnia/src/insomnia-data/src/models/project.ts
@@ -15,10 +15,69 @@ export const SCRATCHPAD_PROJECT_ID = `${prefix}_scratchpad`;
// This is used to identify Git Projects that are not connected to a remote yet
export const EMPTY_GIT_PROJECT_ID = 'empty';
+// Prefix used when encoding a GitRepository._id into gitRepositoryId for downgrade protection.
+// The real GitRepository doc has _id = 'git_xxx'. We store 'gr_xxx' on the project so that the
+// old app's getById('gr_xxx') returns null — preventing it from touching the git folder.
+// The new app swaps the prefix back to recover the real ID.
+export const PROTECTED_GIT_REPO_PREFIX = 'gr_';
+const REAL_GIT_REPO_PREFIX = 'git_';
+
+/**
+ * Decode a raw gitRepositoryId string to the real GitRepository._id.
+ * Handles both the protected ('gr_xxx') and legacy ('git_xxx') formats.
+ */
+export function decodeRepoId(id: string): string {
+ if (id.startsWith(PROTECTED_GIT_REPO_PREFIX)) {
+ return REAL_GIT_REPO_PREFIX + id.slice(PROTECTED_GIT_REPO_PREFIX.length);
+ }
+ return id;
+}
+
+/**
+ * Given a connected GitProject, return the real GitRepository._id.
+ * Returns null when the project is not connected (gitRepositoryId is 'empty').
+ *
+ * Handles two formats:
+ * - 'gr_xxx' → protected (new format) → returns 'git_xxx'
+ * - 'git_xxx' → legacy (pre-migration) → returns 'git_xxx' as-is
+ */
+export function getEffectiveRepoId(project: GitProject): string | null {
+ const id = project.gitRepositoryId;
+ if (id === EMPTY_GIT_PROJECT_ID) return null;
+ return decodeRepoId(id);
+}
+
+/**
+ * Encode a real GitRepository._id ('git_xxx') into the protected format ('gr_xxx')
+ * that is stored on the project's gitRepositoryId field.
+ */
+export function toProtectedRepoId(gitRepositoryId: string): string {
+ if (gitRepositoryId.startsWith(REAL_GIT_REPO_PREFIX)) {
+ return PROTECTED_GIT_REPO_PREFIX + gitRepositoryId.slice(REAL_GIT_REPO_PREFIX.length);
+ }
+ return gitRepositoryId; // already protected or unexpected format — pass through
+}
+
+/**
+ * Return all values that may be stored in Project.gitRepositoryId for a given real
+ * GitRepository._id, covering both legacy ('git_xxx') and protected ('gr_xxx') forms.
+ * Use this when building DB queries that must match projects regardless of which
+ * storage format they were written with.
+ */
+export function getQueryableGitRepositoryIds(gitRepositoryId: string): string[] {
+ const realId = decodeRepoId(gitRepositoryId);
+ const protectedId = toProtectedRepoId(realId);
+ return Array.from(new Set([realId, protectedId]));
+}
+
export function isEmptyGitProject(project: Project) {
return project.gitRepositoryId === EMPTY_GIT_PROJECT_ID;
}
+export function isConnectedGitProject(project: Project): project is GitProject {
+ return isGitProject(project) && getEffectiveRepoId(project) !== null;
+}
+
export const isScratchpadProject = (project: Pick) => project._id === SCRATCHPAD_PROJECT_ID;
export const isLocalProject = (project: Pick): project is LocalProject =>
project.remoteId === null;
diff --git a/packages/insomnia/src/insomnia-data/src/models/settings.ts b/packages/insomnia/src/insomnia-data/src/models/settings.ts
index 7aa65edf1e92..393136f9d77f 100644
--- a/packages/insomnia/src/insomnia-data/src/models/settings.ts
+++ b/packages/insomnia/src/insomnia-data/src/models/settings.ts
@@ -76,6 +76,11 @@ export function init(): BaseSettings {
// The duration in mins for which the external vault secret is cached
vaultSecretCacheDuration: 30,
dataFolders: [],
+ scriptSandboxEnabled: true,
+ scriptStrictModeEnabled: true,
+ disabledSecurityRules: [],
+ disabledBlockedProperties: [],
+ disabledBlockedRoots: [],
npmRegistryUrl: '',
};
}
diff --git a/packages/insomnia/src/main/git-service.ts b/packages/insomnia/src/main/git-service.ts
index 00934e90893c..732d630f184c 100644
--- a/packages/insomnia/src/main/git-service.ts
+++ b/packages/insomnia/src/main/git-service.ts
@@ -61,7 +61,7 @@ import GitVCS, {
import { MemClient } from '../sync/git/mem-client';
import { NeDBClient } from '../sync/git/ne-db-client';
import { projectRoutableFSClient } from '../sync/git/project-routable-fs-client';
-import { repoFileWatcherRegistry } from '../sync/git/repo-file-watcher';
+import { createElectronNotifier, RepoFileWatcherRegistry, type WatcherNotifier } from '../sync/git/repo-file-watcher';
import { routableFSClient } from '../sync/git/routable-fs-client';
import { shallowClone } from '../sync/git/shallow-clone';
import type { AutoResolvedConflict, MergeConflict } from '../sync/types';
@@ -72,6 +72,35 @@ import { ipcMainHandle } from './ipc/electron';
// Initialize Git Remote Providers on module load
initializeGitRemoteProviders();
+/**
+ * Set of repo IDs for which conflict problems should be suppressed in the
+ * file-problems-changed IPC broadcast. Active while the user is resolving
+ * conflicts via SyncMergeModal so the generic "CLI conflict" blocking modal
+ * does not appear on top of the interactive resolver.
+ */
+const suppressedConflictRepos = new Set();
+
+const _electronNotifier = createElectronNotifier();
+const conflictFilteringNotifier: WatcherNotifier = {
+ onDbSynced: () => _electronNotifier.onDbSynced(),
+ onProblemsChanged: payload => {
+ _electronNotifier.onProblemsChanged({
+ ...payload,
+ conflictsSuppressed: suppressedConflictRepos.has(payload.repoId),
+ });
+ },
+};
+
+const repoFileWatcherRegistry = new RepoFileWatcherRegistry(conflictFilteringNotifier);
+
+function suppressConflictProblems(repoId: string): void {
+ suppressedConflictRepos.add(repoId);
+}
+
+function clearConflictSuppression(repoId: string): void {
+ suppressedConflictRepos.delete(repoId);
+}
+
type PushPull = 'push' | 'pull';
type VCSAction =
| PushPull
@@ -189,12 +218,10 @@ async function getGitRepository({ projectId, workspaceId }: { projectId: string;
invariant(projectId, 'Project ID is required');
const project = await services.project.getById(projectId);
invariant(project, 'Project not found');
- invariant(project.gitRepositoryId, 'Project is not linked to a git repository');
- invariant(
- project.gitRepositoryId && !models.project.isEmptyGitProject(project),
- 'Project is not linked to a git repository',
- );
- const gitRepository = await services.gitRepository.getById(project.gitRepositoryId);
+ invariant(models.project.isConnectedGitProject(project), 'Project is not linked to a git repository');
+ const repoId = models.project.getEffectiveRepoId(project);
+ invariant(repoId, 'Project is not linked to a git repository');
+ const gitRepository = await services.gitRepository.getById(repoId);
invariant(gitRepository, 'Git Repository not found');
return gitRepository;
}
@@ -264,22 +291,18 @@ export async function getProjectGitFileIssues({
gitRepositoryId,
}: GetProjectGitFileIssuesOptions): Promise {
const project = await services.project.getById(projectId);
- if (
- !project ||
- !models.project.isGitProject(project) ||
- !project.gitRepositoryId ||
- models.project.isEmptyGitProject(project)
- ) {
+ if (!project || !models.project.isConnectedGitProject(project)) {
return [];
}
- if (gitRepositoryId && gitRepositoryId !== project.gitRepositoryId) {
+ const effectiveRepoId = models.project.getEffectiveRepoId(project);
+ if (gitRepositoryId && gitRepositoryId !== effectiveRepoId) {
return [];
}
return mapWorkspaceFileIssues({
- issues: repoFileWatcherRegistry.getProblems(project.gitRepositoryId),
- repoId: project.gitRepositoryId,
+ issues: repoFileWatcherRegistry.getProblems(effectiveRepoId!),
+ repoId: effectiveRepoId!,
metas: await getProjectWorkspacesWithMeta(projectId),
workspaceId,
});
@@ -1139,7 +1162,7 @@ export const cloneGitRepoAction = async ({
await services.project.update(project, {
remoteId: null,
- gitRepositoryId: gitRepository._id,
+ gitRepositoryId: models.project.toProtectedRepoId(gitRepository._id),
});
return project;
@@ -1148,7 +1171,7 @@ export const cloneGitRepoAction = async ({
const project = await services.project.create({
name: name || gitRepository.uri.split('/').pop() || 'New Git Project',
parentId: organizationId,
- gitRepositoryId: gitRepository._id,
+ gitRepositoryId: models.project.toProtectedRepoId(gitRepository._id),
});
return project;
@@ -1454,7 +1477,8 @@ export const updateGitRepoAction = async ({
let gitRepository: GitRepository | undefined;
if (gitRepositoryId && gitRepositoryId !== models.project.EMPTY_GIT_PROJECT_ID) {
- gitRepository = await services.gitRepository.getById(gitRepositoryId);
+ const effectiveId = models.project.decodeRepoId(gitRepositoryId);
+ gitRepository = await services.gitRepository.getById(effectiveId);
invariant(gitRepository, 'GitRepository not found');
} else {
const newRepo: Partial = {
@@ -1476,7 +1500,7 @@ export const updateGitRepoAction = async ({
const project = await services.project.getById(projectId);
invariant(project, 'Project not found');
await services.project.update(project, {
- gitRepositoryId: gitRepository._id,
+ gitRepositoryId: models.project.toProtectedRepoId(gitRepository._id),
});
}
@@ -1543,6 +1567,7 @@ export const resetGitRepoAction = async ({ projectId, workspaceId }: { projectId
await services.gitRepository.remove(repo);
// Stop the file watcher for this repository (project-scoped flow only).
repoFileWatcherRegistry.stopWatcher(repo._id);
+ clearConflictSuppression(repo._id);
await database.flushChanges(flushId);
@@ -1982,6 +2007,7 @@ export const mergeGitBranch = async ({
const bufferId = await database.bufferChanges();
try {
+ suppressConflictProblems(gitRepository._id);
await GitVCS.merge({
theirsBranch,
allowUncommittedChangesBeforeMerge,
@@ -1993,6 +2019,7 @@ export const mergeGitBranch = async ({
// Import all YAML files from disk into the DB after merge + checkout
const gitRepoId = gitRepository._id;
await repoFileWatcherRegistry.importAllFiles(gitRepoId);
+ clearConflictSuppression(gitRepository._id);
trackSegmentEvent(SegmentEvent.vcsAction, {
...vcsSegmentEventProperties('git', 'merge_branch'),
@@ -2013,8 +2040,10 @@ export const mergeGitBranch = async ({
return {};
} catch (err) {
if (err instanceof MergeConflictError) {
+ // Keep suppression active — user will resolve via SyncMergeModal.
return err.data;
}
+ clearConflictSuppression(gitRepository._id);
let errorMessage = getErrorMessage(err);
if (err instanceof Errors.HttpError) {
@@ -2231,9 +2260,12 @@ export async function fetchGitRemoteBranches({
}
export async function pullFromGitRemote({ projectId, workspaceId }: { projectId: string; workspaceId?: string }) {
+ let repoId: string | null = null;
try {
await assertBranchOnOrigin('pull');
const gitRepository = await getGitRepository({ projectId, workspaceId });
+ repoId = gitRepository._id;
+ suppressConflictProblems(repoId);
invariant(gitRepository.credentialsId, 'Git Credentials ID is required');
const credentials = await services.gitCredentials.getById(gitRepository.credentialsId);
invariant(credentials, 'Git Credentials not found');
@@ -2243,6 +2275,7 @@ export async function pullFromGitRemote({ projectId, workspaceId }: { projectId:
// Import all YAML files from disk into the DB after pull
await repoFileWatcherRegistry.importAllFiles(gitRepository._id);
+ clearConflictSuppression(repoId);
trackSegmentEvent(SegmentEvent.vcsAction, {
...vcsSegmentEventProperties('git', 'pull'),
@@ -2266,9 +2299,13 @@ export async function pullFromGitRemote({ projectId, workspaceId }: { projectId:
};
} catch (err: unknown) {
if (err instanceof MergeConflictError) {
+ // Keep suppression active — user will resolve via SyncMergeModal.
+ // clearConflictSuppression is called by continueMerge or abortMergeAction.
return err.data;
}
+ if (repoId) clearConflictSuppression(repoId);
+
if (
err instanceof Errors.UserCanceledError ||
(err instanceof Errors.HttpError && (err.data.statusCode === 401 || err.data.statusCode === 403))
@@ -2334,6 +2371,9 @@ export const continueMerge = async ({
// Import all YAML files from disk into the DB after merge resolution
await repoFileWatcherRegistry.importAllFiles(gitRepository._id);
+ // Files are clean now — lift the conflict suppression so any remaining
+ // issues (parse errors etc.) are reported normally.
+ clearConflictSuppression(gitRepository._id);
const log = (await GitVCS.log({ depth: 1 })) || [];
@@ -2414,7 +2454,9 @@ export const discardChangesAction = async ({
}
};
-export const abortMergeAction = async () => {
+export const abortMergeAction = async ({ projectId, workspaceId }: { projectId: string; workspaceId?: string }) => {
+ const gitRepository = await getGitRepository({ projectId, workspaceId });
+ clearConflictSuppression(gitRepository._id);
return GitVCS.abortMerge();
};
@@ -2851,14 +2893,12 @@ export async function runAllGitRepoMigrations(): Promise {
const failedProjects: { id: string; name: string }[] = [];
const allProjects = await services.project.all();
- const gitProjects = allProjects.filter(
- (p): p is GitProject => models.project.isGitProject(p) && !models.project.isEmptyGitProject(p),
- );
+ const gitProjects = allProjects.filter((p): p is GitProject => models.project.isConnectedGitProject(p));
if (gitProjects.length === 0) return { logs, failedProjects };
// Batch-fetch all git repositories in one query instead of N individual lookups.
- const repoIds = gitProjects.map(p => p.gitRepositoryId);
+ const repoIds = gitProjects.map(p => models.project.getEffectiveRepoId(p)).filter(Boolean) as string[];
const gitRepositories = await database.find(models.gitRepository.type, {
_id: { $in: repoIds },
});
@@ -2869,11 +2909,13 @@ export async function runAllGitRepoMigrations(): Promise {
const ts = () => new Date().toISOString();
const projectList = gitProjects.map(p => `"${p.name}"`).join(', ');
- logs.push(`${ts()} [INFO] Starting migration v${CURRENT_MIGRATION_VERSION} for ${gitProjects.length} repo(s): ${projectList}`);
+ logs.push(
+ `${ts()} [INFO] Starting migration v${CURRENT_MIGRATION_VERSION} for ${gitProjects.length} repo(s): ${projectList}`,
+ );
await Promise.all(
gitProjects.map(async project => {
- const gitRepository = repoById.get(project.gitRepositoryId);
+ const gitRepository = repoById.get(models.project.getEffectiveRepoId(project)!);
if (!gitRepository) return;
const repoId = gitRepository._id;
@@ -2901,15 +2943,16 @@ export async function runAllGitRepoMigrations(): Promise {
logs.push(`${ts()} [INFO] ["${name}"] Converting to local project`);
try {
const project = await services.project.getById(id);
- if (!project || !project.gitRepositoryId) {
+ if (!project || !models.project.isConnectedGitProject(project)) {
logs.push(`${ts()} [WARN] ["${name}"] Project not found or already local — skipping`);
return;
}
- const gitRepository = await services.gitRepository.getById(project.gitRepositoryId);
+ const effectiveRepoId = models.project.getEffectiveRepoId(project as GitProject);
+ const gitRepository = effectiveRepoId ? await services.gitRepository.getById(effectiveRepoId) : null;
if (gitRepository) {
await services.gitRepository.remove(gitRepository);
- logs.push(`${ts()} [INFO] ["${name}"] Removed git repository ${project.gitRepositoryId}`);
+ logs.push(`${ts()} [INFO] ["${name}"] Removed git repository ${effectiveRepoId}`);
}
await services.project.update(project, { name, gitRepositoryId: null });
@@ -3036,7 +3079,7 @@ export const registerGitServiceAPI = () => {
ipcMainHandle('git.discardChanges', (_, options: Parameters[0]) =>
discardChangesAction(options),
);
- ipcMainHandle('git.abortMerge', _ => abortMergeAction());
+ ipcMainHandle('git.abortMerge', (_, options: Parameters[0]) => abortMergeAction(options));
ipcMainHandle('git.gitStatus', (_, options: Parameters[0]) => gitStatusAction(options));
ipcMainHandle('git.diff', () => diff());
ipcMainHandle('git.stageChanges', (_, options: Parameters[0]) =>
diff --git a/packages/insomnia/src/routes/git-migration.$.tsx b/packages/insomnia/src/routes/git-migration.$.tsx
index 742f82bd4b33..93700a5d482d 100644
--- a/packages/insomnia/src/routes/git-migration.$.tsx
+++ b/packages/insomnia/src/routes/git-migration.$.tsx
@@ -3,9 +3,10 @@ import { Link } from 'react-router';
import { Button } from '~/basic-components/button';
import { CopyButton } from '~/ui/components/base/copy-button';
+import { Link as ExternalLink } from '~/ui/components/base/link';
import { InsomniaLogo } from '~/ui/components/insomnia-icon';
import { TrailLinesContainer } from '~/ui/components/trail-lines-container';
-import git_for_all from '~/ui/images/onboarding/git_for_all.png';
+import git_migration from '~/ui/images/git-migration/git.png';
type MigrationStatus = 'default' | 'running' | 'completed' | 'partiallyCompleted' | 'error';
@@ -73,15 +74,10 @@ const MigrationView = () => {
<>
We hit an unexpected error while updating your file system. Please try again.
- If the issue persists, please{' '}
-
- raise a support ticket.
- {' '}
- You may also re-install the previous version by following the steps{' '}
-
- here
-
- .
+ Having trouble and need to contact us, or back up to an old version? See our{' '}
+
+ docs.
+
>
) : (
@@ -90,6 +86,13 @@ const MigrationView = () => {
In order to continue with this update, we need to adjust your local file system. This is required to
enable managing Insomnia changes using git on the CLI.
+
+ Note: This change is backwards compatible, but we strongly recommend{' '}
+
+ following these best practices
+ {' '}
+ when returning to an earlier version of Insomnia.
+
{isUpdateRunning
? 'Note: Your data is safe and the update only takes seconds.'
@@ -101,7 +104,7 @@ const MigrationView = () => {
{isUpdateCompletedSuccessfully ? (
Open Insomnia
@@ -117,7 +120,7 @@ const MigrationView = () => {
Copy Error Logs
Open Insomnia
@@ -134,7 +137,7 @@ const MigrationView = () => {
Copy Error Logs
@@ -143,7 +146,7 @@ const MigrationView = () => {
) : (
@@ -180,13 +183,13 @@ const Component = () => {
Now you can use traditional git actions on your CLI to manage changes to your Git Sync projects.
-
+
{
setShowMigrationView(true);
}}
diff --git a/packages/insomnia/src/routes/git.all-connected-repos.tsx b/packages/insomnia/src/routes/git.all-connected-repos.tsx
index 767d232ae068..72f33ccf83ec 100644
--- a/packages/insomnia/src/routes/git.all-connected-repos.tsx
+++ b/packages/insomnia/src/routes/git.all-connected-repos.tsx
@@ -22,18 +22,17 @@ export async function clientLoader() {
const organizationMap = Object.fromEntries(organizations.map(o => [o.id, o]));
- const allConnectedGitProjects = allProjects.filter(
- project => models.project.isGitProject(project) && !models.project.isEmptyGitProject(project),
- );
+ const allConnectedGitProjects = allProjects.filter(project => models.project.isConnectedGitProject(project));
const gitRepoURIInfoMap: Record = {};
await Promise.all(
- allConnectedGitProjects.map(async ({ gitRepositoryId, name, parentId }) => {
+ allConnectedGitProjects.map(async project => {
+ const gitRepositoryId = models.project.isGitProject(project) ? models.project.getEffectiveRepoId(project) : null;
if (gitRepositoryId) {
const gitRepository = await services.gitRepository.getById(gitRepositoryId);
if (gitRepository) {
gitRepoURIInfoMap[gitRepository.uri] = {
- organizationName: organizationMap[parentId]?.name || '',
- projectName: name,
+ organizationName: organizationMap[project.parentId]?.name || '',
+ projectName: project.name,
};
}
}
diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx
index 5d27f16f77a6..16ff7c29d27e 100644
--- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx
+++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId._index.tsx
@@ -136,7 +136,9 @@ export async function getProjectsWithGitRepositories({
parentId: organizationId,
});
- const gitRepositoryIds = projects.map(p => p.gitRepositoryId).filter(isNotNullOrUndefined);
+ const gitRepositoryIds = projects
+ .map(p => (models.project.isConnectedGitProject(p) ? models.project.getEffectiveRepoId(p) : null))
+ .filter(isNotNullOrUndefined);
const gitRepositories = await database.find('GitRepository', {
_id: {
@@ -145,7 +147,10 @@ export async function getProjectsWithGitRepositories({
});
return projects.map(project => {
- const gitRepository = gitRepositories.find(gr => gr._id === project.gitRepositoryId);
+ const effectiveId = models.project.isConnectedGitProject(project)
+ ? models.project.getEffectiveRepoId(project)
+ : null;
+ const gitRepository = gitRepositories.find(gr => gr._id === effectiveId);
return {
...project,
gitRepository,
@@ -403,8 +408,8 @@ export async function clientLoader({ params }: LoaderFunctionArgs) {
const projectsSyncStatusPromise = CheckAllProjectSyncStatus(projects);
const activeProjectGitRepository =
- project && models.project.isGitProject(project)
- ? await services.gitRepository.getById(project.gitRepositoryId || '')
+ project && models.project.isConnectedGitProject(project)
+ ? await services.gitRepository.getById(models.project.getEffectiveRepoId(project) || '')
: null;
return {
diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.delete.tsx
index 342c13a78956..d916d2120de2 100644
--- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.delete.tsx
+++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.delete.tsx
@@ -4,6 +4,7 @@ import { href, redirect } from 'react-router';
import { database } from '~/common/database';
import { projectLock } from '~/common/project';
import { services } from '~/insomnia-data';
+import * as models from '~/models';
import { reportGitProjectCount } from '~/routes/organization.$organizationId.project.new';
import { invariant } from '~/utils/invariant';
import { createFetcherSubmitHook, getInitialRouteForOrganization } from '~/utils/router';
@@ -32,8 +33,9 @@ export async function clientAction({ params }: Route.ClientActionArgs) {
});
}
- if (project.gitRepositoryId) {
- const gitRepository = await services.gitRepository.getById(project.gitRepositoryId);
+ if (models.project.isConnectedGitProject(project)) {
+ const effectiveRepoId = models.project.isGitProject(project) ? models.project.getEffectiveRepoId(project) : null;
+ const gitRepository = effectiveRepoId ? await services.gitRepository.getById(effectiveRepoId) : null;
gitRepository && (await services.gitRepository.remove(gitRepository));
}
diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx
index 9060dae2fc84..dbd29abf384a 100644
--- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx
+++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx
@@ -29,8 +29,8 @@ export function useProjectLoaderData() {
const Component = () => {
const data = useProjectLoaderData();
const gitRepositoryId =
- data && models.project.isGitProject(data.activeProject) && !models.project.isEmptyGitProject(data.activeProject)
- ? data.activeProject.gitRepositoryId
+ data && models.project.isConnectedGitProject(data.activeProject)
+ ? models.project.getEffectiveRepoId(data.activeProject)
: null;
const gitFileIssues = useProjectGitFileIssues({
projectId: data?.activeProject._id,
diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.update.tsx
index 77022bf44db9..72f2a6d15e83 100644
--- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.update.tsx
+++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.update.tsx
@@ -40,7 +40,8 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
const project = await services.project.getById(projectId);
invariant(project, 'Project not found');
- const gitRepository = project.gitRepositoryId ? await services.gitRepository.getById(project.gitRepositoryId) : null;
+ const effectiveRepoId = models.project.isGitProject(project) ? models.project.getEffectiveRepoId(project) : null;
+ const gitRepository = effectiveRepoId ? await services.gitRepository.getById(effectiveRepoId) : null;
const user = await services.userSession.getOrCreate();
const sessionId = user.id;
@@ -165,8 +166,8 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
},
});
- if (project.gitRepositoryId) {
- const gitRepository = await services.gitRepository.getById(project.gitRepositoryId);
+ if (models.project.isConnectedGitProject(project)) {
+ const gitRepository = await services.gitRepository.getById(models.project.getEffectiveRepoId(project) || '');
gitRepository && (await services.gitRepository.remove(gitRepository));
}
@@ -337,7 +338,8 @@ export async function clientAction({ request, params }: Route.ClientActionArgs)
// convert from git to local
if (storageType === 'local' && project.gitRepositoryId) {
- const gitRepository = await services.gitRepository.getById(project.gitRepositoryId);
+ const effectiveId = models.project.isGitProject(project) ? models.project.getEffectiveRepoId(project) : null;
+ const gitRepository = effectiveId ? await services.gitRepository.getById(effectiveId) : null;
gitRepository && (await services.gitRepository.remove(gitRepository));
await services.project.update(project, { name, gitRepositoryId: null });
diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx
index 72c983ecf34c..6b96e41ec3dc 100644
--- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx
+++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx
@@ -27,8 +27,8 @@ export async function clientAction({ params }: Route.ClientActionArgs) {
const isLintError = (result: IRuleResult) => result.severity === 0;
- const gitRepositoryId = models.project.isGitProject(project)
- ? project.gitRepositoryId
+ const gitRepositoryId = models.project.isConnectedGitProject(project)
+ ? models.project.getEffectiveRepoId(project)
: workspaceMeta?.gitRepositoryId;
const rulesetPath = gitRepositoryId
diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx
index 30d20c9a9a50..7e3755f53e68 100644
--- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx
+++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx
@@ -84,8 +84,8 @@ export async function clientLoader({ params }: Route.ClientLoaderArgs) {
const workspaceMeta = await services.workspaceMeta.getByParentId(workspaceId);
- const gitRepositoryId = models.project.isGitProject(project)
- ? project.gitRepositoryId
+ const gitRepositoryId = models.project.isConnectedGitProject(project)
+ ? models.project.getEffectiveRepoId(project)
: workspaceMeta?.gitRepositoryId;
// we don't run the lint here because it is expensive and slows first render too much
// TODO: add this in once we run this loader outside the renderer
diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx
index e178001938e5..39e6f770cd8e 100644
--- a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx
+++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx
@@ -103,8 +103,8 @@ export async function clientLoader({ params, request }: Route.ClientLoaderArgs)
const activeWorkspaceMeta = await services.workspaceMeta.getOrCreateByParentId(workspaceId);
- const gitRepositoryId = models.project.isGitProject(activeProject)
- ? activeProject.gitRepositoryId
+ const gitRepositoryId = models.project.isConnectedGitProject(activeProject)
+ ? models.project.getEffectiveRepoId(activeProject)
: activeWorkspaceMeta.gitRepositoryId;
const gitRepository = await services.gitRepository.getById(gitRepositoryId || '');
@@ -401,7 +401,7 @@ const Component = () => {
projectId: string;
workspaceId: string;
};
- const { issuesByWorkspaceId } = useGitFileIssues();
+ const { issuesByWorkspaceId, conflictsSuppressed } = useGitFileIssues();
const currentIssue = issuesByWorkspaceId[workspaceId];
const handleBackToList = () => {
@@ -414,7 +414,9 @@ const Component = () => {
};
const modalText = currentIssue ? workspaceFileIssueModalText[currentIssue.kind] : null;
- const isIssueModalOpen = Boolean(currentIssue && modalText);
+ const isIssueModalOpen = Boolean(
+ currentIssue && modalText && !(currentIssue.kind === 'conflict' && conflictsSuppressed),
+ );
return (
diff --git a/packages/insomnia/src/script-executor.ts b/packages/insomnia/src/script-executor.ts
index 8d0503ee410d..a85cd8fc4827 100644
--- a/packages/insomnia/src/script-executor.ts
+++ b/packages/insomnia/src/script-executor.ts
@@ -11,7 +11,7 @@ import {
mergeSettings,
type RequestContext,
} from '../../insomnia-scripting-environment/src/objects';
-import { requireInterceptor } from './require-interceptor';
+import { requireInterceptor } from './scripting/require-interceptor';
import { invariant } from './utils/invariant';
export const runScript = async ({
diff --git a/packages/insomnia/src/scripting/__tests__/require-interceptor.test.ts b/packages/insomnia/src/scripting/__tests__/require-interceptor.test.ts
new file mode 100644
index 000000000000..6043d28ecf6d
--- /dev/null
+++ b/packages/insomnia/src/scripting/__tests__/require-interceptor.test.ts
@@ -0,0 +1,142 @@
+import { describe, expect, it } from 'vitest';
+
+import { requireInterceptor } from '../require-interceptor';
+
+const allows = (moduleName: string) =>
+ expect(() => requireInterceptor(moduleName)).not.toThrow();
+
+const blocks = (moduleName: string) =>
+ expect(() => requireInterceptor(moduleName)).toThrow();
+
+describe('requireInterceptor', () => {
+ describe('blocked modules', () => {
+ it('blocks child_process', () => blocks('child_process'));
+ it('blocks fs', () => blocks('fs'));
+ it('blocks os', () => blocks('os'));
+ it('blocks net', () => blocks('net'));
+ it('blocks http', () => blocks('http'));
+ it('blocks https', () => blocks('https'));
+ it('blocks crypto', () => blocks('crypto'));
+ it('blocks vm', () => blocks('vm'));
+ it('blocks worker_threads', () => blocks('worker_threads'));
+ it('blocks unknown module', () => blocks('some-unknown-module'));
+ });
+
+ describe('node built-ins', () => {
+ it('allows path', () => allows('path'));
+ it('allows assert', () => allows('assert'));
+ it('allows url', () => allows('url'));
+ it('allows punycode', () => allows('punycode'));
+ it('allows querystring', () => allows('querystring'));
+ it('allows string_decoder', () => allows('string_decoder'));
+ it('allows stream', () => allows('stream'));
+ it('allows events', () => allows('events'));
+ });
+
+ describe('timers', () => {
+ it('allows timers', () => allows('timers'));
+
+ // it('strips setImmediate from timers', () => {
+ // const timers = requireInterceptor('timers');
+ // expect(timers.setImmediate).toBeUndefined();
+ // });
+
+ it('strips queueMicrotask from timers', () => {
+ const timers = requireInterceptor('timers');
+ expect(timers.queueMicrotask).toBeUndefined();
+ });
+
+ it('preserves setTimeout in timers', () => {
+ const timers = requireInterceptor('timers');
+ expect(timers.setTimeout).toBeDefined();
+ });
+
+ it('preserves setInterval in timers', () => {
+ const timers = requireInterceptor('timers');
+ expect(timers.setInterval).toBeDefined();
+ });
+ });
+
+ describe('buffer', () => {
+ it('allows buffer', () => allows('buffer'));
+
+ it('blocks Buffer.allocUnsafe', () => {
+ const { Buffer: SafeBuffer } = requireInterceptor('buffer');
+ expect(() => SafeBuffer.allocUnsafe(8)).toThrow('Buffer.allocUnsafe is not available in sandbox scripts');
+ });
+
+ it('blocks Buffer.allocUnsafeSlow', () => {
+ const { Buffer: SafeBuffer } = requireInterceptor('buffer');
+ expect(() => SafeBuffer.allocUnsafeSlow(8)).toThrow('Buffer.allocUnsafeSlow is not available in sandbox scripts');
+ });
+
+ it('allows Buffer.alloc', () => {
+ const { Buffer: SafeBuffer } = requireInterceptor('buffer');
+ expect(() => SafeBuffer.alloc(8)).not.toThrow();
+ });
+
+ it('allows Buffer.from', () => {
+ const { Buffer: SafeBuffer } = requireInterceptor('buffer');
+ expect(() => SafeBuffer.from('hello')).not.toThrow();
+ });
+ });
+
+ describe('util', () => {
+ it('allows util', () => allows('util'));
+
+ it('blocks util.inherits', () => {
+ const util = requireInterceptor('util');
+ expect(() => util.inherits()).toThrow('util.inherits is not available in sandbox scripts');
+ });
+
+ it('blocks util.debuglog', () => {
+ const util = requireInterceptor('util');
+ expect(() => util.debuglog()).toThrow('util.debuglog is not available in sandbox scripts');
+ });
+
+ it('allows util.format', () => {
+ const util = requireInterceptor('util');
+ expect(() => util.format('%s', 'hello')).not.toThrow();
+ });
+
+ it('allows util.inspect', () => {
+ const util = requireInterceptor('util');
+ expect(() => util.inspect({})).not.toThrow();
+ });
+ });
+
+ describe('external modules', () => {
+ it('allows ajv', () => allows('ajv'));
+ it('allows chai', () => allows('chai'));
+ it('allows cheerio', () => allows('cheerio'));
+ it('allows crypto-js', () => allows('crypto-js'));
+ it('allows csv-parse/lib/sync', () => allows('csv-parse/lib/sync'));
+ it('allows lodash', () => allows('lodash'));
+ it('allows moment', () => allows('moment'));
+ it('allows tv4', () => allows('tv4'));
+ it('allows uuid', () => allows('uuid'));
+ it('allows xml2js', () => allows('xml2js'));
+ });
+
+ describe('base64 helpers', () => {
+ it('allows atob', () => allows('atob'));
+ it('allows btoa', () => allows('btoa'));
+
+ it('atob returns a function', () => {
+ expect(typeof requireInterceptor('atob')).toBe('function');
+ });
+
+ it('btoa returns a function', () => {
+ expect(typeof requireInterceptor('btoa')).toBe('function');
+ });
+ });
+
+ describe('collection modules', () => {
+ it('allows insomnia-collection', () => allows('insomnia-collection'));
+ it('allows postman-collection', () => allows('postman-collection'));
+
+ it('insomnia-collection and postman-collection return the same module', () => {
+ expect(requireInterceptor('insomnia-collection')).toBe(requireInterceptor('postman-collection'));
+ });
+ });
+});
diff --git a/packages/insomnia/src/scripting/__tests__/sandbox.test.ts b/packages/insomnia/src/scripting/__tests__/sandbox.test.ts
new file mode 100644
index 000000000000..dd72129cdeb5
--- /dev/null
+++ b/packages/insomnia/src/scripting/__tests__/sandbox.test.ts
@@ -0,0 +1,267 @@
+import { describe, expect, it } from 'vitest';
+
+import { checkSandboxViolations } from '../sandbox';
+import { blockedPropertyRules, blockedRootRules } from '../script-security-policy';
+
+const ALL_BLOCKED_PROPERTIES = new Set(blockedPropertyRules.map(r => r.name));
+const ALL_BLOCKED_ROOTS = new Set(blockedRootRules.map(r => r.name));
+
+const check = (script: string, props = ALL_BLOCKED_PROPERTIES, roots = ALL_BLOCKED_ROOTS) =>
+ () => checkSandboxViolations(script, props, roots);
+
+const blocked = (script: string) => expect(check(script)).toThrow();
+const allowed = (script: string) => expect(check(script)).not.toThrow();
+
+const withoutProperty = (name: string) =>
+ new Set([...ALL_BLOCKED_PROPERTIES].filter(p => p !== name));
+
+const withoutRoot = (name: string) =>
+ new Set([...ALL_BLOCKED_ROOTS].filter(r => r !== name));
+
+// ---------------------------------------------------------------------------
+// Blocked properties — one canonical script per rule covering both dot and
+// bracket notation where applicable. The unblocking section below mirrors
+// each rule to confirm the disable path works too.
+// ---------------------------------------------------------------------------
+
+describe('checkSandboxViolations', () => {
+
+ describe('blocked properties — dot notation', () => {
+ it('blocks prototype', () => blocked('Promise.prototype.then'));
+ it('blocks mainModule', () => blocked('proc.mainModule'));
+ it('blocks constructor', () => blocked('obj.constructor'));
+ it('blocks __proto__', () => blocked('obj.__proto__'));
+ it('blocks prepareStackTrace', () => blocked('Error.prepareStackTrace'));
+ it('blocks captureStackTrace', () => blocked('Error.captureStackTrace'));
+ it('blocks getPrototypeOf', () => blocked('Object.getPrototypeOf(target)'));
+ it('blocks setPrototypeOf', () => blocked('Object.setPrototypeOf(obj, null)'));
+ it('blocks getFunction', () => blocked('frame.getFunction()'));
+ it('blocks getThis', () => blocked('frame.getThis()'));
+ it('blocks __defineGetter__', () => blocked('obj.__defineGetter__("foo", fn)'));
+ it('blocks __defineSetter__', () => blocked('obj.__defineSetter__("foo", fn)'));
+ it('blocks __lookupGetter__', () => blocked('obj.__lookupGetter__("foo")'));
+ it('blocks __lookupSetter__', () => blocked('obj.__lookupSetter__("foo")'));
+ it('blocks defineProperty', () => blocked('Object.defineProperty(obj, "key", desc)'));
+ it('blocks defineProperties', () => blocked('Object.defineProperties(obj, descs)'));
+ it('blocks getOwnPropertyDescriptor', () => blocked('Object.getOwnPropertyDescriptor(obj, "key")'));
+ it('blocks getOwnPropertyDescriptors', () => blocked('Object.getOwnPropertyDescriptors(obj)'));
+ });
+
+ describe('blocked properties — bracket notation', () => {
+ it('blocks constructor', () => blocked('obj["constructor"]'));
+ it('blocks __proto__', () => blocked('obj["__proto__"]'));
+ it('blocks prototype', () => blocked('Promise["prototype"]'));
+ it('blocks prepareStackTrace', () => blocked('Error["prepareStackTrace"]'));
+ it('blocks captureStackTrace', () => blocked('Error["captureStackTrace"]'));
+ it('blocks defineProperty', () => blocked('Object["defineProperty"](obj, "key", desc)'));
+ });
+
+ // ---------------------------------------------------------------------------
+ // Blocked roots
+ // ---------------------------------------------------------------------------
+
+ describe('blocked roots — direct member access', () => {
+ it('blocks this', () => blocked('this.x'));
+ it('blocks globalThis', () => blocked('globalThis.require'));
+ it('blocks global', () => blocked('global.require'));
+ it('blocks window', () => blocked('window.process'));
+ it('blocks self', () => blocked('self.process'));
+ it('blocks frames', () => blocked('frames[0]'));
+ it('blocks process', () => blocked('process.env'));
+ it('blocks module', () => blocked('module.exports'));
+ it('blocks exports', () => blocked('exports.foo'));
+ it('blocks Buffer', () => blocked('Buffer.from("data")'));
+ it('blocks arguments', () => blocked('arguments[0]'));
+ });
+
+ describe('blocked roots — direct call', () => {
+ it('blocks constructor called directly', () =>
+ blocked('constructor("return process")()'));
+ });
+
+ describe('blocked roots — bracket notation', () => {
+ it('blocks globalThis["require"]', () => blocked('globalThis["require"]()'));
+ it('blocks window["process"]', () => blocked('window["process"]'));
+ it('blocks self["require"]', () => blocked('self["require"]'));
+ it('blocks process["env"]', () => blocked('process["env"]'));
+ });
+
+ // ---------------------------------------------------------------------------
+ // Alias chains and destructuring
+ // ---------------------------------------------------------------------------
+
+ describe('this — alias chains and destructuring', () => {
+ it('blocks this.process.mainModule.require via member', () =>
+ blocked(`this.process.mainModule.require('child_process')`));
+
+ it('blocks this["process"]', () =>
+ blocked(`this['process']`));
+
+ it('blocks dynamic key on this', () =>
+ blocked(`const k = 'process'; this[k]`));
+
+ it('blocks const alias: const t = this; t.process', () =>
+ blocked(`const t = this; t.process.mainModule.require('child_process')`));
+
+ it('blocks assignment alias: let t; t = this; t.process', () =>
+ blocked(`let t; t = this; t.process.mainModule.require('child_process')`));
+
+ it('blocks destructuring from this', () =>
+ blocked(`const { process } = this`));
+
+ it('blocks destructuring assignment from this', () =>
+ blocked(`({ process } = this)`));
+ });
+
+ describe('globalThis — alias chains and destructuring', () => {
+ it('blocks const alias: const g = globalThis; g.require', () =>
+ blocked(`const g = globalThis; g.require('child_process')`));
+
+ it('blocks destructuring from globalThis', () =>
+ blocked(`const { require } = globalThis`));
+
+ it('blocks destructuring assignment from globalThis', () =>
+ blocked(`({ require } = globalThis)`));
+ });
+
+ // ---------------------------------------------------------------------------
+ // Prototype chain mutation
+ // ---------------------------------------------------------------------------
+
+ describe('prototype chain mutation', () => {
+ it('blocks Promise.prototype.then mutation', () =>
+ blocked(`Promise.prototype.then = function(fn) { fn.call(globalThis); }`));
+
+ it('blocks Promise.prototype.catch mutation', () =>
+ blocked(`Promise.prototype.catch = function() {}`));
+
+ it('blocks Array.prototype.map mutation', () =>
+ blocked(`Array.prototype.map = function() {}`));
+
+ it('blocks Function.prototype.call mutation', () =>
+ blocked(`Function.prototype.call = function() {}`));
+
+ it('blocks reading Promise.prototype', () =>
+ blocked(`const proto = Promise.prototype`));
+
+ it('blocks bracket notation on Promise.prototype', () =>
+ blocked(`Promise['prototype']`));
+ });
+
+ // ---------------------------------------------------------------------------
+ // Dynamic import
+ // ---------------------------------------------------------------------------
+
+ describe('import', () => {
+ it('blocks dynamic import()', () =>
+ blocked(`import('child_process')`));
+
+ it('blocks dynamic import() with variable', () =>
+ blocked(`const m = 'child_process'; import(m)`));
+
+ it('blocks static import declaration', () =>
+ blocked(`import fs from 'fs'`));
+
+ it('blocks static import with named exports', () =>
+ blocked(`import { readFile } from 'fs'`));
+ });
+
+ // ---------------------------------------------------------------------------
+ // Symbol.species
+ // ---------------------------------------------------------------------------
+
+ describe('Symbol.species', () => {
+ it('blocks Symbol.species', () =>
+ blocked(`Symbol.species`));
+ });
+
+ // ---------------------------------------------------------------------------
+ // Unblocking — disabling a rule must allow previously blocked scripts
+ // ---------------------------------------------------------------------------
+
+ describe('unblocking — disabling a blocked property rule allows the script', () => {
+ const cases: [name: string, script: string][] = [
+ ['prototype', 'Promise.prototype.then'],
+ ['mainModule', 'proc.mainModule'],
+ ['constructor', 'obj.constructor'],
+ ['__proto__', 'obj.__proto__'],
+ ['prepareStackTrace', 'Error.prepareStackTrace'],
+ ['captureStackTrace', 'Error.captureStackTrace'],
+ ['getPrototypeOf', 'Object.getPrototypeOf(target)'],
+ ['setPrototypeOf', 'Object.setPrototypeOf(obj, null)'],
+ ['getFunction', 'frame.getFunction()'],
+ ['getThis', 'frame.getThis()'],
+ ['__defineGetter__', 'obj.__defineGetter__("foo", fn)'],
+ ['__defineSetter__', 'obj.__defineSetter__("foo", fn)'],
+ ['__lookupGetter__', 'obj.__lookupGetter__("foo")'],
+ ['__lookupSetter__', 'obj.__lookupSetter__("foo")'],
+ ['defineProperty', 'Object.defineProperty(obj, "key", desc)'],
+ ['defineProperties', 'Object.defineProperties(obj, descs)'],
+ ['getOwnPropertyDescriptor', 'Object.getOwnPropertyDescriptor(obj, "key")'],
+ ['getOwnPropertyDescriptors','Object.getOwnPropertyDescriptors(obj)'],
+ ];
+
+ for (const [name, script] of cases) {
+ it(`disabling '${name}' allows: ${script}`, () =>
+ expect(check(script, withoutProperty(name))).not.toThrow());
+ }
+ });
+
+ describe('unblocking — disabling a blocked root rule allows the script', () => {
+ const cases: [name: string, script: string][] = [
+ ['this', 'this.x'],
+ ['globalThis', 'globalThis.require'],
+ ['global', 'global.require'],
+ ['window', 'window.process'],
+ ['self', 'self.process'],
+ ['frames', 'frames[0]'],
+ ['process', 'process.env'],
+ ['module', 'module.exports'],
+ ['exports', 'exports.foo'],
+ ['Buffer', 'Buffer.from("data")'],
+ ['constructor', 'constructor("return process")()'],
+ ['arguments', 'arguments[0]'],
+ ];
+
+ for (const [name, script] of cases) {
+ it(`disabling '${name}' allows: ${script}`, () =>
+ expect(check(script, ALL_BLOCKED_PROPERTIES, withoutRoot(name))).not.toThrow());
+ }
+
+ it('disabling this also allows const aliases of this', () =>
+ expect(check('const t = this; t.x', ALL_BLOCKED_PROPERTIES, withoutRoot('this'))).not.toThrow());
+
+ it('disabling globalThis also allows const aliases of globalThis', () =>
+ expect(check('const g = globalThis; g.require', ALL_BLOCKED_PROPERTIES, withoutRoot('globalThis'))).not.toThrow());
+ });
+
+ // ---------------------------------------------------------------------------
+ // Allowed scripts
+ // ---------------------------------------------------------------------------
+
+ describe('allowed scripts', () => {
+ it('allows normal variable declarations', () =>
+ allowed(`const x = 1 + 2`));
+
+ it('allows require() calls', () =>
+ allowed(`require('lodash')`));
+
+ it('allows insomnia API usage', () =>
+ allowed(`insomnia.environment.set('key', 'val')`));
+
+ it('allows async/await', () =>
+ allowed(`const res = await insomnia.sendRequest('https://example.com')`));
+
+ it('allows pm.test()', () =>
+ allowed(`pm.test('status is 200', () => { pm.expect(pm.response.code).to.equal(200); })`));
+
+ it('allows lodash usage', () =>
+ allowed(`const val = _.get(obj, 'foo.bar')`));
+
+ it('allows console.log', () =>
+ allowed(`console.log('hello')`));
+
+ it('allows class with prototype-like property name in string', () =>
+ allowed(`const key = 'prototype'; obj[key]`));
+ });
+});
diff --git a/packages/insomnia/src/scripting/__tests__/script-security-policy.test.ts b/packages/insomnia/src/scripting/__tests__/script-security-policy.test.ts
new file mode 100644
index 000000000000..0dd424538985
--- /dev/null
+++ b/packages/insomnia/src/scripting/__tests__/script-security-policy.test.ts
@@ -0,0 +1,190 @@
+import { describe, expect, it } from 'vitest';
+
+import { requireInterceptor } from '../require-interceptor';
+import { defaultSecurityPolicy } from '../sandbox';
+import { interceptorRules, maskRules } from '../script-security-policy';
+
+// Build the mask map once — shared across all tests.
+const { names, values } = defaultSecurityPolicy.buildMaskScope();
+const maskMap = new Map(names.map((name, i) => [name, values[i]]));
+
+describe('ScriptSecurityPolicy.buildMaskScope()', () => {
+ describe('coverage — every rule with a maskName is present', () => {
+ it('includes all interceptor rule mask names', () => {
+ for (const rule of interceptorRules) {
+ if (rule.maskName) {
+ expect(names, `missing mask for interceptor rule "${rule.name}"`).toContain(rule.maskName);
+ }
+ }
+ });
+
+ it('includes all mask rule names', () => {
+ for (const rule of maskRules) {
+ if (rule.maskName) {
+ expect(names, `missing mask for mask rule "${rule.name}"`).toContain(rule.maskName);
+ }
+ }
+ });
+ });
+
+ describe('mask rules — blocked globals resolve to undefined', () => {
+ const undefinedMasks = [
+ 'globalThis',
+ 'global',
+ 'Function',
+ 'process',
+ 'setImmediate',
+ 'queueMicrotask',
+ 'Proxy',
+ 'Reflect',
+ 'WebAssembly',
+ ];
+
+ for (const name of undefinedMasks) {
+ it(`${name} → undefined`, () => {
+ expect(maskMap.has(name)).toBe(true);
+ expect(maskMap.get(name)).toBeUndefined();
+ });
+ }
+ });
+
+ describe('require interceptor', () => {
+ it('masks require with requireInterceptor', () => {
+ expect(maskMap.get('require')).toBe(requireInterceptor);
+ });
+ });
+
+ describe('window allowlist', () => {
+ // In Vitest's Node environment window is undefined, so the rule returns undefined.
+ it('masks window to undefined in Node environment', () => {
+ expect(maskMap.has('window')).toBe(true);
+ expect(maskMap.get('window')).toBeUndefined();
+ });
+ });
+
+ describe('eval interceptor', () => {
+ const evalFn = maskMap.get('eval') as (script: string) => unknown;
+
+ it('is a function', () => {
+ expect(typeof evalFn).toBe('function');
+ });
+
+ it('throws on null input', () => {
+ expect(() => (evalFn as any)(null)).toThrow();
+ });
+
+ it('throws on non-string input', () => {
+ expect(() => (evalFn as any)(42)).toThrow();
+ });
+
+ describe('blocks AST violations smuggled through eval', () => {
+ it('blocks dynamic import()', () => {
+ expect(() => evalFn('import("child_process")')).toThrow();
+ });
+
+ it('blocks globalThis access', () => {
+ expect(() => evalFn('globalThis.process')).toThrow();
+ });
+
+ it('blocks constructor access', () => {
+ expect(() => evalFn('obj.constructor')).toThrow();
+ });
+
+ it('blocks __proto__ access', () => {
+ expect(() => evalFn('obj.__proto__')).toThrow();
+ });
+
+ it('blocks prototype access', () => {
+ expect(() => evalFn('Promise.prototype')).toThrow();
+ });
+
+ it('blocks setPrototypeOf access', () => {
+ expect(() => evalFn('Object.setPrototypeOf(obj, null)')).toThrow();
+ });
+
+ it('blocks captureStackTrace access', () => {
+ expect(() => evalFn('Error.captureStackTrace(obj)')).toThrow();
+ });
+
+ it('blocks defineProperty access', () => {
+ expect(() => evalFn('Object.defineProperty(obj, "key", {})')).toThrow();
+ });
+ });
+
+ describe('allows safe eval', () => {
+ it('evaluates arithmetic', () => {
+ expect(evalFn('1 + 1')).toBe(2);
+ });
+
+ it('evaluates string expressions', () => {
+ expect(evalFn('"hello"')).toBe('hello');
+ });
+ });
+ });
+});
+
+describe('ScriptSecurityPolicy builders', () => {
+ describe('withoutRule()', () => {
+ it('removes a rule by name', () => {
+ const policy = defaultSecurityPolicy.withoutRule('process');
+ const { names } = policy.buildMaskScope();
+ expect(names).not.toContain('process');
+ });
+
+ it('leaves other rules intact', () => {
+ const policy = defaultSecurityPolicy.withoutRule('process');
+ const { names } = policy.buildMaskScope();
+ expect(names).toContain('globalThis');
+ });
+
+ it('is a no-op for an unknown rule name', () => {
+ const before = defaultSecurityPolicy.buildMaskScope().names.length;
+ const after = defaultSecurityPolicy.withoutRule('nonexistent').buildMaskScope().names.length;
+ expect(after).toBe(before);
+ });
+
+ it('can remove each mask rule individually', () => {
+ for (const rule of maskRules) {
+ const policy = defaultSecurityPolicy.withoutRule(rule.name);
+ const { names } = policy.buildMaskScope();
+ if (rule.maskName) {
+ expect(names, `rule '${rule.name}' was not removed`).not.toContain(rule.maskName);
+ }
+ }
+ });
+
+ it('can remove each interceptor rule individually', () => {
+ for (const rule of interceptorRules) {
+ const policy = defaultSecurityPolicy.withoutRule(rule.name);
+ const { names } = policy.buildMaskScope();
+ if (rule.maskName) {
+ expect(names, `rule '${rule.name}' was not removed`).not.toContain(rule.maskName);
+ }
+ }
+ });
+ });
+
+ describe('withRule()', () => {
+ it('appends a new mask rule', () => {
+ const policy = defaultSecurityPolicy.withRule({
+ name: 'custom-mask',
+ description: 'test rule',
+ maskName: 'customGlobal',
+ maskValue: undefined,
+ });
+ const { names } = policy.buildMaskScope();
+ expect(names).toContain('customGlobal');
+ });
+
+ it('does not mutate the original policy', () => {
+ defaultSecurityPolicy.withRule({
+ name: 'custom-mask',
+ description: 'test rule',
+ maskName: 'customGlobal',
+ maskValue: undefined,
+ });
+ const { names } = defaultSecurityPolicy.buildMaskScope();
+ expect(names).not.toContain('customGlobal');
+ });
+ });
+});
diff --git a/packages/insomnia/src/require-interceptor.ts b/packages/insomnia/src/scripting/require-interceptor.ts
similarity index 53%
rename from packages/insomnia/src/require-interceptor.ts
rename to packages/insomnia/src/scripting/require-interceptor.ts
index 44cd75c1ae70..76e102939640 100644
--- a/packages/insomnia/src/require-interceptor.ts
+++ b/packages/insomnia/src/scripting/require-interceptor.ts
@@ -9,7 +9,7 @@ import tv4 from 'tv4';
import * as uuid from 'uuid';
import xml2js from 'xml2js';
-import { Collection as CollectionModule } from '../../insomnia-scripting-environment/src/objects';
+import { Collection as CollectionModule } from '../../../insomnia-scripting-environment/src/objects';
const externalModules = new Map
([
['ajv', ajv],
@@ -24,20 +24,46 @@ const externalModules = new Map([
['xml2js', xml2js],
]);
+// wraps `target` with a Proxy restricting access to dangerious methods within the accepted modules.
+const blockMethods = (target: object, blocked: string[], label: string): object =>
+ new Proxy(target, {
+ get(t, prop) {
+ if (typeof prop === 'string' && blocked.includes(prop)) {
+ throw new Error(`${label}.${prop} is not available in sandbox scripts`);
+ }
+ const value = (t as any)[prop];
+ return typeof value === 'function' ? value.bind(t) : value;
+ },
+ });
+
export const requireInterceptor = (moduleName: string): any => {
- if (
+ if (moduleName === 'timers') {
+ // Block setImmediate
+ return blockMethods(require('node:timers'), ['setImmediate'], 'timers');
+ } else if (moduleName === 'buffer') {
+ // Block unsafe allocation methods to prevent heap memory disclosure.
+ // Buffer.allocUnsafe(n) / Buffer.allocUnsafeSlow(n) return a buffer backed by uninitialized memory.
+ const bufferModule = require('node:buffer');
+ return {
+ ...bufferModule,
+ Buffer: blockMethods(bufferModule.Buffer, ['allocUnsafe', 'allocUnsafeSlow'], 'Buffer'),
+ };
+ } else if (moduleName === 'util') {
+ // Block escape utils like util.inherits and util.debuglog
+ // util.inherits(ctor, superCtor) — directly manipulates the prototype chain (
+ // util.debuglog(section) — conditionally writes to stderr based on the NODE_DEBUG environment variable
+ return blockMethods(require('node:util'), ['inherits', 'debuglog'], 'util');
+
+ } else if (
[
// node.js modules
'path',
'assert',
- 'buffer',
- 'util',
'url',
'punycode',
'querystring',
'string_decoder',
'stream',
- 'timers',
'events',
// follows should be npm modules
// but they are moved to here to avoid introducing additional dependencies
diff --git a/packages/insomnia/src/scripting/run-script.ts b/packages/insomnia/src/scripting/run-script.ts
new file mode 100644
index 000000000000..c2dbbc4bc03a
--- /dev/null
+++ b/packages/insomnia/src/scripting/run-script.ts
@@ -0,0 +1,154 @@
+import * as _ from 'es-toolkit/compat';
+
+import {
+ InsomniaObject,
+ mergeClientCertificates,
+ mergeCookieJar,
+ mergeRequests,
+ mergeSettings,
+ type RequestContext,
+} from '../../../insomnia-scripting-environment/src/objects';
+import { defaultSecurityPolicy, prepareSandbox, ScriptSecurityPolicy } from './sandbox';
+
+export const runScript = async ({
+ script,
+ context,
+ securityPolicy = defaultSecurityPolicy,
+}: {
+ script: string;
+ context: RequestContext;
+ securityPolicy?: ScriptSecurityPolicy;
+}): Promise => {
+ const activePolicy = context.settings.scriptSandboxEnabled !== false
+ ? securityPolicy
+ : new ScriptSecurityPolicy([]);
+
+ const {
+ executionContext,
+ scriptConsole,
+ maskNames,
+ maskValues,
+ bridgeOps,
+ } = await prepareSandbox(script, context, activePolicy);
+
+ const AsyncFunction = (async () => {}).constructor;
+ const scriptParams = [
+ 'insomnia', // insomnia scripting API object
+ 'console', // log console
+ '_', // lodash library
+ 'setTimeout', // proxied setTimeout tracked by the async task monitor
+ '__waitForAllTestsDone__', // Drains pm.test() assertions before the script exits
+ '__bridgeReset__', // Clears the async task list and re-enables monitoring
+ '__bridgeStop__', // Stops recording new promises into the task list
+ '__bridgeSettle__', // Awaits all tracked promises before returning
+ ...maskNames, // Masked globals from the security policy (e.g. eval → undefined)
+ ];
+ const strictMode = context.settings.scriptStrictModeEnabled !== false;
+ const scriptBody = [
+ `__bridgeReset__();`, // Start with a clean async task slate for this script run
+ `await (async function() {`, // IIFE gives the user script its own lexical scope
+ ...(strictMode ? [` 'use strict';`] : []), // Strict mode: this === undefined, prevents silent errors
+ ` const $ = insomnia;`, // Postman-compat alias for the insomnia scripting object
+ ` ${script}`, // User script body
+ `})();`,
+ `await __waitForAllTestsDone__();`, // Wait for all pm.test() callbacks to resolve
+ `__bridgeStop__();`, // Stop tracking new promises (user script is done)
+ `await __bridgeSettle__();`, // Drain any fire-and-forget promises the script created
+ `return insomnia;`, // Return the (possibly mutated) insomnia context
+ ].join('\n');
+
+ // const scriptBody = [
+ // `const $ = insomnia;`,
+ // `__bridgeReset__();`,
+ // `try {`,
+ // ` ${script}`,
+ // ` await __waitForAllTestsDone__();`,
+ // `} finally {`,
+ // ` __bridgeStop__();`,
+ // ` await __bridgeSettle__();`,
+ // `}`,
+ // `return insomnia;`,
+ // ].join('\n');
+
+ const executeScript = AsyncFunction(...scriptParams, scriptBody);
+
+ const mutatedInsomniaObject = await executeScript(
+ executionContext,
+ scriptConsole,
+ _,
+ proxiedSetTimeout,
+ bridgeOps.waitForAllTestsDone,
+ bridgeOps.resetAsyncTasks,
+ bridgeOps.stopMonitorAsyncTasks,
+ bridgeOps.asyncTasksAllSettled,
+ ...maskValues,
+ );
+
+ if (mutatedInsomniaObject == null || !(mutatedInsomniaObject instanceof InsomniaObject)) {
+ throw new Error('insomnia object is invalid or script returns earlier than expected.');
+ }
+
+ const mutatedContextObject = mutatedInsomniaObject.toObject();
+ const updatedRequest = mergeRequests(context.request, mutatedContextObject.request);
+ const updatedSettings = mergeSettings(context.settings, mutatedContextObject.request);
+ const updatedCertificates = mergeClientCertificates(
+ mutatedContextObject.clientCertificates,
+ mutatedContextObject.request,
+ );
+ const updatedCookieJar = mergeCookieJar(context.cookieJar, mutatedContextObject.cookieJar);
+
+ return {
+ ...context,
+ environment: {
+ id: context.environment.id,
+ name: context.environment.name,
+ data: mutatedContextObject.environment,
+ },
+ baseEnvironment: {
+ id: context.baseEnvironment.id,
+ name: context.baseEnvironment.name,
+ data: mutatedContextObject.baseEnvironment,
+ },
+ iterationData: context.iterationData
+ ? {
+ name: context.iterationData.name,
+ data: mutatedContextObject.iterationData,
+ }
+ : undefined,
+ transientVariables: {
+ name: context.transientVariables?.name || 'transientVariables',
+ data: mutatedContextObject.variables,
+ },
+ request: updatedRequest,
+ execution: mutatedContextObject.execution,
+ settings: updatedSettings,
+ clientCertificates: updatedCertificates,
+ cookieJar: updatedCookieJar,
+ globals: context.globals && {
+ id: context.globals.id,
+ name: context.globals.name,
+ data: mutatedContextObject.globals,
+ },
+ baseGlobals: context.baseGlobals && {
+ id: context.baseGlobals.id,
+ name: context.baseGlobals.name,
+ data: mutatedContextObject.baseGlobals,
+ },
+ requestTestResults: mutatedContextObject.requestTestResults,
+ logs: scriptConsole.dumpLogsAsArray(),
+ parentFolders: mutatedContextObject.parentFolders,
+ };
+};
+
+function proxiedSetTimeout(callback: () => void, ms?: number | undefined) {
+ let resolveHdl: (value: unknown) => void;
+
+ new Promise(resolve => {
+ resolveHdl = resolve;
+ });
+
+ return setTimeout(() => {
+ callback();
+ resolveHdl(null);
+ }, ms);
+}
diff --git a/packages/insomnia/src/scripting/sandbox.ts b/packages/insomnia/src/scripting/sandbox.ts
new file mode 100644
index 000000000000..b9883cf62e10
--- /dev/null
+++ b/packages/insomnia/src/scripting/sandbox.ts
@@ -0,0 +1,357 @@
+import * as acorn from 'acorn';
+import * as walk from "acorn-walk";
+
+import {
+ getNewConsole,
+ initInsomniaObject,
+ type RequestContext,
+ waitForAllTestsDone,
+} from '../../../insomnia-scripting-environment/src/objects';
+import { blockedPropertyRules, blockedRootRules, interceptorRules, maskRules, type ThreatRule } from './script-security-policy';
+
+// Frozen, pre-bound references to the bridge lifecycle methods
+export interface BridgeOps {
+ resetAsyncTasks: () => void;
+ stopMonitorAsyncTasks: () => void;
+ asyncTasksAllSettled: () => Promise;
+ waitForAllTestsDone: () => Promise;
+}
+
+export interface SandboxContext {
+ executionContext: Awaited>;
+ scriptConsole: ReturnType;
+ maskNames: string[];
+ maskValues: unknown[];
+ bridgeOps: BridgeOps;
+}
+
+// Derive the default blocked sets from the canonical rule lists in script-security-policy.
+const SANDBOX_BLOCKED_PROPERTIES = new Set(blockedPropertyRules.map(r => r.name));
+const SANDBOX_BLOCKED_ROOTS = new Set(blockedRootRules.map(r => r.name));
+
+// These interceptor rules always apply — they cannot be disabled via settings and run even when
+// the sandbox is turned off, because they gate access to critical host APIs (require, window, eval).
+const ALWAYS_ON_INTERCEPTORS = new Set(['require', 'window', 'eval']);
+
+// Walks a MemberExpression down to its root Identifier.
+function getMemberRoot(node: any): string | null {
+ if (node.type === 'Identifier') return node.name;
+ if (node.type === 'MemberExpression') return getMemberRoot(node.object);
+ return null;
+}
+
+ // Returns MemberExpression property name.
+function getMemberPropertyName(node: acorn.MemberExpression): string | null {
+ if (!node.computed && node.property.type === 'Identifier') {
+ return (node.property as acorn.Identifier).name;
+ }
+ if (node.computed && node.property.type === 'Literal') {
+ const val = (node.property as acorn.Literal).value;
+ return typeof val === 'string' ? val : null;
+ }
+ return null;
+}
+
+function deepFreeze(obj: T): T {
+ const propNames = Object.getOwnPropertyNames(obj);
+ for (const name of propNames) {
+ const value = (obj as any)[name];
+ if (value && typeof value === "object") {
+ deepFreeze(value);
+ }
+ }
+ return Object.freeze(obj);
+}
+
+// parses `script` and checks for sandbox policy violations.
+// Pass custom sets to apply per-rule overrides from user settings.
+export function checkSandboxViolations(
+ script: string,
+ blockedProperties: Set = SANDBOX_BLOCKED_PROPERTIES,
+ blockedRoots: Set = SANDBOX_BLOCKED_ROOTS,
+): void {
+ let tree: any;
+ for (const sourceType of ['module', 'script'] as const) {
+ try {
+ tree = acorn.parse(script, { ecmaVersion: 2022, sourceType });
+ break;
+ } catch {
+ // try next sourceType
+ }
+ }
+ // We should evenutally drop non-valid JavaScript.
+ if (!tree) {
+ // throw new Error();
+ return;
+ }
+
+ // Maps each blocked name to its root origin so error messages can explain alias chains.
+ // e.g. `const s = this; s.x` → blocked.get('s') === 'this'
+ const blocked = new Map();
+ for (const root of blockedRoots) {
+ blocked.set(root, root);
+ }
+
+ // Helper: render a name with its origin chain if aliased (e.g. "'s' → 'this'").
+ const label = (name: string) => {
+ const origin = blocked.get(name);
+ return origin && origin !== name ? `'${name}' → '${origin}'` : `'${name}'`;
+ };
+
+ // Helper: returns the specific rule name to tell the user to disable.
+ // For aliases the root origin is the actual named rule; for direct roots it's the name itself.
+ const ruleHint = (name: string) => {
+ const origin = blocked.get(name) ?? name;
+ return `'${origin}'`;
+ };
+
+ walk.simple(tree, {
+ // const/let/var g = globalThis OR const s = this
+ VariableDeclarator(node: acorn.VariableDeclarator) {
+ if (node.id.type !== 'Identifier') return;
+ const id = node.id as acorn.Identifier;
+ if (node.init?.type === 'Identifier') {
+ const initName = (node.init as acorn.Identifier).name;
+ const origin = blocked.get(initName);
+ if (origin !== undefined) {
+ blocked.set(id.name, origin);
+ }
+ }
+ // `this` is a ThisExpression, not an Identifier — handle separately.
+ // Only track aliases if the 'this' rule is active in the current policy.
+ if (node.init?.type === 'ThisExpression' && blockedRoots.has('this')) {
+ blocked.set(id.name, 'this');
+ }
+ },
+ // g = globalThis (bare assignment) OR s = this
+ AssignmentExpression(node: acorn.AssignmentExpression) {
+ if (node.left.type !== 'Identifier') return;
+ const id = node.left as acorn.Identifier;
+ if (node.right.type === 'Identifier') {
+ const rightName = (node.right as acorn.Identifier).name;
+ const origin = blocked.get(rightName);
+ if (origin !== undefined) {
+ blocked.set(id.name, origin);
+ }
+ }
+ if (node.right.type === 'ThisExpression' && blockedRoots.has('this')) {
+ blocked.set(id.name, 'this');
+ }
+ },
+ });
+
+ // check for violations using the fully expanded blocked map.
+ walk.simple(tree, {
+ MemberExpression(node: acorn.MemberExpression) {
+ if (node.object.type === 'ThisExpression' && blockedRoots.has('this')) {
+ throw new Error(
+ `The script was blocked because it used 'this'.\n` +
+ `If this is intended, disable it via Settings → Scripting → Blocked roots.`,
+ );
+ }
+
+ // Covers dot and computed bracket notation via root chain traversal.
+ const root = getMemberRoot(node.object);
+ if (root && blocked.has(root)) {
+ throw new Error(
+ `The script was blocked because it used ${label(root)}.\n` +
+ `If this is intended, disable ${ruleHint(root)} via Settings → Scripting → Blocked roots.`,
+ );
+ }
+
+ // obj.constructor, obj['__proto__'], obj.getPrototypeOf, etc.
+ const prop = getMemberPropertyName(node);
+ if (prop && blockedProperties.has(prop)) {
+ throw new Error(
+ `The script was blocked because it used the property '${prop}'.\n` +
+ `If this is intended, disable '${prop}' via Settings → Scripting → Blocked properties.`,
+ );
+ }
+
+ // Symbol.species: Promise[Symbol.species] / Array[Symbol.species].
+ if (
+ node.object.type === 'Identifier' &&
+ (node.object as acorn.Identifier).name === 'Symbol' &&
+ prop === 'species'
+ ) {
+ throw new Error(
+ `The script was blocked because it used Symbol.species.\n` +
+ `If this is intended, disable 'species' via Settings → Scripting → Blocked properties.`,
+ );
+ }
+ },
+ VariableDeclarator(node: acorn.VariableDeclarator) {
+ if (node.id.type !== 'ObjectPattern') return;
+ // Destructuring declaration: const { require } = globalThis
+ if (
+ node.init?.type === 'Identifier' &&
+ blocked.has((node.init as acorn.Identifier).name)
+ ) {
+ const initName = (node.init as acorn.Identifier).name;
+ throw new Error(
+ `The script was blocked because it destructured from ${label(initName)}.\n` +
+ `If this is intended, disable ${ruleHint(initName)} via Settings → Scripting → Blocked roots.`,
+ );
+ }
+ // Destructuring from this: const { process } = this
+ if (node.init?.type === 'ThisExpression' && blockedRoots.has('this')) {
+ throw new Error(
+ `The script was blocked because it destructured from 'this'.\n` +
+ `If this is intended, disable it via Settings → Scripting → Blocked roots.`,
+ );
+ }
+ },
+ AssignmentExpression(node: acorn.AssignmentExpression) {
+ // Destructuring assignment: ({ require } = globalThis)
+ if (
+ node.left.type === 'ObjectPattern' &&
+ node.right.type === 'Identifier' &&
+ blocked.has((node.right as acorn.Identifier).name)
+ ) {
+ const rightName = (node.right as acorn.Identifier).name;
+ throw new Error(
+ `The script was blocked because it destructured from ${label(rightName)}.\n` +
+ `If this is intended, disable ${ruleHint(rightName)} via Settings → Scripting → Blocked roots.`,
+ );
+ }
+ // Destructuring assignment from this: ({ process } = this)
+ if (node.left.type === 'ObjectPattern' && node.right.type === 'ThisExpression' && blockedRoots.has('this')) {
+ throw new Error(
+ `The script was blocked because it destructured from 'this'.\n` +
+ `If this is intended, disable it via Settings → Scripting → Blocked roots.`,
+ );
+ }
+ },
+ // Static import declaration: import fs from 'fs'
+ ImportDeclaration(_node: acorn.ImportDeclaration) {
+ throw new Error(
+ `The script was blocked because it used a static import declaration.\n` +
+ `If this is intended, disable 'eval-intercept' via Settings → Scripting → Enable script sandbox.`,
+ );
+ },
+ // Dynamic import(): import('node:child_process')
+ ImportExpression(_node: acorn.Node) {
+ throw new Error(
+ `The script was blocked because it used a dynamic import().\n` +
+ `If this is intended, disable 'eval-intercept' via Settings → Scripting → Enable script sandbox.`,
+ );
+ },
+ // Direct call of a blocked identifier: constructor('return process')()
+ // Not caught by MemberExpression since there is no property access involved.
+ CallExpression(node: acorn.CallExpression) {
+ if (
+ node.callee.type === 'Identifier' &&
+ blocked.has((node.callee as acorn.Identifier).name)
+ ) {
+ const calleeName = (node.callee as acorn.Identifier).name;
+ throw new Error(
+ `The script was blocked because it called ${label(calleeName)}.\n` +
+ `If this is intended, disable ${ruleHint(calleeName)} via Settings → Scripting → Blocked roots.`,
+ );
+ }
+ },
+ });
+}
+
+// Builds and applies the runtime security policy for user-supplied scripts.
+// Extend with `.withRule()` or reduce with `.withoutRule()`.
+export class ScriptSecurityPolicy {
+ constructor(private readonly rules: ThreatRule[]) {}
+
+ // returns a policy with `rule` appended (immutable).
+ withRule(rule: ThreatRule): ScriptSecurityPolicy {
+ return new ScriptSecurityPolicy([...this.rules, rule]);
+ }
+
+ // returns a policy with the named rule removed (immutable).
+ withoutRule(name: string): ScriptSecurityPolicy {
+ return new ScriptSecurityPolicy(this.rules.filter(r => r.name !== name));
+ }
+
+ // returns parallel `names` / `values` arrays for all rules that carry a runtime mask.
+ // Pass `violationCheck` to forward the caller's filtered checker (e.g. to eval-intercept).
+ buildMaskScope(violationCheck: (script: string) => void = checkSandboxViolations): { names: string[]; values: unknown[] } {
+ const names: string[] = [];
+ const values: unknown[] = [];
+ for (const rule of this.rules) {
+ if (rule.maskName !== undefined) {
+ names.push(rule.maskName);
+ values.push(
+ rule.buildMaskValue !== undefined
+ ? rule.buildMaskValue(violationCheck)
+ : rule.maskValue,
+ );
+ }
+ }
+ return { names, values };
+ }
+}
+
+// Default policy (runtime interceptors and masks).
+export const defaultSecurityPolicy = new ScriptSecurityPolicy([
+ ...interceptorRules,
+ ...maskRules,
+]);
+
+// runs all pre-execution security checks and initialises the script environment.
+// 1. AST blockes globals, dangerous properties, aliasing, destructuring, dynamic import, and symbol.species.
+// 2. mask scope returns the parallel names/values arrays
+export async function prepareSandbox(
+ script: string,
+ context: RequestContext,
+ securityPolicy: ScriptSecurityPolicy = defaultSecurityPolicy,
+): Promise {
+ const scriptConsole = getNewConsole();
+
+ let sandboxContext = context;
+ let maskNames: string[] = [];
+ let maskValues: unknown[] = [];
+
+ if (context.settings.scriptSandboxEnabled !== false) {
+ const disabledProps = new Set(context.settings.disabledBlockedProperties);
+ const disabledRoots = new Set(context.settings.disabledBlockedRoots);
+ const activeProperties = new Set([...SANDBOX_BLOCKED_PROPERTIES].filter(p => !disabledProps.has(p)));
+ const activeRoots = new Set([...SANDBOX_BLOCKED_ROOTS].filter(r => !disabledRoots.has(r)));
+
+ // Bind the filtered checker so eval-intercept uses the same active policy.
+ const activeSandboxCheck = (s: string) => checkSandboxViolations(s, activeProperties, activeRoots);
+
+ try {
+ activeSandboxCheck(script);
+ } catch (err) {
+ const error = err instanceof Error ? err : new Error(String(err));
+ (error as NodeJS.ErrnoException).code = 'SECURITY_POLICY_VIOLATION';
+ throw error;
+ }
+
+ // prevents mutate via insomnia._settings.
+ sandboxContext = { ...context, settings: deepFreeze({ ...context.settings }) };
+ // Always-on interceptors cannot be disabled via settings — filter them out before applying user overrides.
+ const disabledRules = (context.settings.disabledSecurityRules ?? []).filter(
+ name => !ALWAYS_ON_INTERCEPTORS.has(name),
+ );
+ const activePolicy = disabledRules.reduce(
+ (policy, ruleName) => policy.withoutRule(ruleName),
+ securityPolicy,
+ );
+ ({ names: maskNames, values: maskValues } = activePolicy.buildMaskScope(activeSandboxCheck));
+ } else {
+ console.warn('[sandbox] script sandbox is disabled — running script without security checks');
+ // Even with the sandbox off, always apply the require/window/eval interceptors.
+ const alwaysOnPolicy = new ScriptSecurityPolicy(
+ interceptorRules.filter(r => ALWAYS_ON_INTERCEPTORS.has(r.name)),
+ );
+ ({ names: maskNames, values: maskValues } = alwaysOnPolicy.buildMaskScope(checkSandboxViolations));
+ }
+
+ const executionContext = await initInsomniaObject(sandboxContext, scriptConsole.log);
+
+ const bridgeOps: BridgeOps = {
+ resetAsyncTasks: Object.freeze(window.bridge.resetAsyncTasks.bind(window.bridge)),
+ stopMonitorAsyncTasks: Object.freeze(window.bridge.stopMonitorAsyncTasks.bind(window.bridge)),
+ asyncTasksAllSettled: Object.freeze(window.bridge.asyncTasksAllSettled.bind(window.bridge)),
+ waitForAllTestsDone: Object.freeze(waitForAllTestsDone),
+ };
+
+ return { executionContext, scriptConsole, maskNames, maskValues, bridgeOps };
+}
diff --git a/packages/insomnia/src/scripting/script-security-policy.ts b/packages/insomnia/src/scripting/script-security-policy.ts
new file mode 100644
index 000000000000..5e566891709d
--- /dev/null
+++ b/packages/insomnia/src/scripting/script-security-policy.ts
@@ -0,0 +1,162 @@
+import { invariant } from '../utils/invariant';
+import { requireInterceptor } from './require-interceptor';
+
+export interface ASTRule {
+ name: string; // the identifier / property name being blocked.
+ description: string;
+}
+
+export const blockedPropertyRules: ASTRule[] = [
+ { name: 'prototype', description: 'Prototype mutation — direct assignment (e.g. Promise.prototype.then = ...) can corrupt built-ins for all code in the sandbox.' },
+ { name: 'mainModule', description: 'Prevents accessing the reference property to the top-level module object.' },
+ { name: 'constructor', description: 'Prevents accessing .constructor on any object.' },
+ { name: '__proto__', description: 'Prototype mutation — direct prototype chain manipulation; can reassign an object\'s prototype to a host object.' },
+ { name: 'prepareStackTrace', description: 'Stack inspection escape — V8 stack trace hook (CVE-2023-29017, CVE-2023-30547); a crafted Error can run arbitrary code during stringify.' },
+ { name: 'captureStackTrace', description: 'Stack inspection — V8 method that captures the current call stack onto an object, exposing stack frame host objects.' },
+ { name: 'getPrototypeOf', description: 'Prototype chain traversal — can reach the .constructor of a host object and reconstruct Function.' },
+ { name: 'setPrototypeOf', description: 'Prototype mutation — directly replaces an object\'s prototype, enabling prototype chain manipulation at runtime.' },
+ { name: 'getFunction', description: 'Stack inspection — V8 CallSite method that leaks unsanitised host objects from the call stack.' },
+ { name: 'getThis', description: 'Stack inspection — V8 CallSite method that leaks the unsanitised receiver of each stack frame.' },
+ { name: '__defineGetter__', description: 'Accessor helper — deprecated method that bypasses the normal property descriptor flow.' },
+ { name: '__defineSetter__', description: 'Accessor helper — deprecated method that bypasses the normal property descriptor flow.' },
+ { name: '__lookupGetter__', description: 'Accessor helper — deprecated method that can be used to inspect hidden property descriptors.' },
+ { name: '__lookupSetter__', description: 'Accessor helper — deprecated method that can be used to inspect hidden property descriptors.' },
+ { name: 'defineProperty', description: 'Property descriptor manipulation — installs arbitrary getters, setters, or non-configurable properties on any object including built-ins.' },
+ { name: 'defineProperties', description: 'Property descriptor manipulation — same as defineProperty but for multiple properties at once.' },
+ { name: 'getOwnPropertyDescriptor', description: 'Property descriptor inspection — returns the full descriptor including any getter/setter functions, which may be host objects.' },
+ { name: 'getOwnPropertyDescriptors', description: 'Property descriptor inspection — returns all property descriptors at once; same risk as getOwnPropertyDescriptor.' },
+];
+
+export const blockedRootRules: ASTRule[] = [
+ { name: 'this', description: 'Global object access — in the outer AsyncFunction scope (non-strict) \'this\' is the host global object, with the same reach as globalThis.' },
+ { name: 'globalThis', description: 'Global object access — primary global object alias that exposes every host API that parameter masking is meant to hide.' },
+ { name: 'global', description: 'Global object access — Node.js alias for globalThis; dynamic access (e.g. global["req"+"uire"]) bypasses string-literal detection.' },
+ { name: 'window', description: 'Global object access — browser global alias; inside Electron it also reaches Node.js APIs via window.bridge and similar.' },
+ { name: 'self', description: 'Global object access — Web Worker / browser alias for globalThis; available in some Electron renderer contexts.' },
+ { name: 'frames', description: 'Global object access — browser alias for the window.frames collection; can be used to navigate to an unsandboxed global.' },
+ { name: 'process', description: 'Node.js internals access — exposes mainModule, env, and other Node.js internals not part of the supported scripting API.' },
+ { name: 'module', description: 'Module system bypass — Node.js module wrapper object; .require and .children expose the full module graph.' },
+ { name: 'exports', description: 'Module system bypass — Node.js module exports object; mutating it affects the live module cache.' },
+ { name: 'Buffer', description: 'Unsafe memory access — the Buffer global provides allocUnsafe(), which reads uninitialised memory.' },
+ { name: 'constructor', description: 'Function constructor escape — in AsyncFunction scope this IS AsyncFunction; a direct call constructs a new function in the real global scope.' },
+ { name: 'arguments', description: 'Caller inspection — can leak the caller\'s frame in generator or sloppy-mode contexts, exposing host objects.' },
+];
+
+export interface ThreatRule {
+ name: string; // unique rule id.
+ description: string; // message detailing the block reason.
+ maskName?: string; // identifier to mask in the script's function scope
+ maskValue?: unknown; // value bound to `maskName`. (normally `undefined` or a interceptor function).
+ buildMaskValue?: (violationCheck: (script: string) => void) => unknown; // Factory called at buildMaskScope() time. Receives checkSandboxViolations so interceptors can perform full static analysis on dynamic input (e.g. eval strings).
+}
+
+// mask interceptor binding rules.
+export const interceptorRules: ThreatRule[] = [
+ {
+ name: 'require',
+ description: 'Replaces the require() function with an interceptor to prevent access to modules outside an explicit allowlist.',
+ maskName: 'require',
+ maskValue: requireInterceptor,
+ },
+ {
+ name: 'window',
+ description: 'Replaces the window object with a restricted proxy to prevent access to host APIs beyond the three bridge methods the script executor requires.',
+ maskName: 'window',
+ buildMaskValue: _violationCheck => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+ const allowedBridgeMethods = new Set([
+ 'resetAsyncTasks',
+ 'stopMonitorAsyncTasks',
+ 'asyncTasksAllSettled',
+ ]);
+ const bridgeProxy = new Proxy(window.bridge, {
+ get(target, prop: string | symbol) {
+ if (allowedBridgeMethods.has(prop)) {
+ return Reflect.get(target, prop);
+ }
+ return;
+ },
+ });
+ return new Proxy(window, {
+ get(_target, prop: string | symbol) {
+ if (prop === 'bridge') {
+ return bridgeProxy;
+ }
+ return;
+ },
+ });
+ },
+ },
+ {
+ name: 'eval',
+ description: 'Replaces the eval() function with an interceptor to prevent execution of scripts containing sandbox violations.',
+ maskName: 'eval',
+ buildMaskValue: violationCheck => (script: string) => {
+ invariant(script && typeof script === 'string', 'eval is called with invalid or empty value');
+ violationCheck(script);
+
+
+ return (0, eval)(script);
+ },
+ },
+];
+
+// Runtime masks — bindings replaced with undefined to make them unreachable.
+export const maskRules: ThreatRule[] = [
+ {
+ name: 'globalThis',
+ description: 'Prevents access to the globalThis object to prevent exposure of process, require, and other host APIs that parameter masking is meant to hide.',
+ maskName: 'globalThis',
+ maskValue: undefined,
+ },
+ {
+ name: 'global',
+ description: 'Prevents access to the global parameter (Node.js alias for globalThis) to prevent dynamic access to host APIs (e.g. global["req"+"uire"]).',
+ maskName: 'global',
+ maskValue: undefined,
+ },
+ {
+ name: 'Function',
+ description: 'Prevents access to the Function constructor to prevent creation of new functions in the real global scope, escaping parameter-level masking (e.g. Function("return process")()).',
+ maskName: 'Function',
+ maskValue: undefined,
+ },
+ {
+ name: 'process',
+ description: 'Prevents access to the process object to prevent exposure of mainModule, env, and other Node.js internals not part of the supported scripting API.',
+ maskName: 'process',
+ maskValue: undefined,
+ },
+ {
+ name: 'setImmediate',
+ description: 'Prevents access to the setImmediate function to prevent its use as an untracked async scheduling side-channel.',
+ maskName: 'setImmediate',
+ maskValue: undefined,
+ },
+ {
+ name: 'queueMicrotask',
+ maskName: 'queueMicrotask',
+ description: 'Prevents access to the queueMicrotask function to prevent scheduling work outside the async/await flow tracked by the executor, which would make clean shutdown harder.',
+ maskValue: undefined,
+ },
+ {
+ name: 'Proxy',
+ description: 'Prevents access to the Proxy constructor to prevent apply/construct traps from receiving unwrapped host objects, which enables prototype chain traversal to real host globals (CVE-2023-32314).',
+ maskName: 'Proxy',
+ maskValue: undefined,
+ },
+ {
+ name: 'Reflect',
+ description: 'Prevents access to the Reflect object to prevent Reflect.apply() and Reflect.construct() from invoking functions with an explicit this value, bypassing the strict-mode this===undefined invariant.',
+ maskName: 'Reflect',
+ maskValue: undefined,
+ },
+ {
+ name: 'WebAssembly',
+ description: 'Prevents access to the WebAssembly API to prevent loading and executing arbitrary native bytecode, which would bypass JS-level sandboxing entirely.',
+ maskName: 'WebAssembly',
+ maskValue: undefined,
+ },
+];
diff --git a/packages/insomnia/src/sync/git/repo-file-watcher.ts b/packages/insomnia/src/sync/git/repo-file-watcher.ts
index 13b90cc9be5b..f52745e8d034 100644
--- a/packages/insomnia/src/sync/git/repo-file-watcher.ts
+++ b/packages/insomnia/src/sync/git/repo-file-watcher.ts
@@ -73,6 +73,8 @@ export interface FileProblemsChangedPayload {
repoId: string;
problems: FileIssue[];
workspaceIssues: WorkspaceFileIssue[];
+ /** True when the main process is suppressing conflict display (e.g. SyncMergeModal is open). */
+ conflictsSuppressed: boolean;
}
/** Compute a SHA-256 hex digest of a string. */
@@ -145,6 +147,11 @@ class RepoFileWatcher {
// 1. Load workspace-to-file mappings from the DB for rename detection.
await watcher.loadKnownGitFilePaths();
+ // 1b. If the DB has newer data than what’s on disk (e.g. the user edited
+ // requests on the old app during a downgrade), write fresh YAML to
+ // disk BEFORE importing so those edits are not silently overwritten.
+ await watcher.flushNewerDbWorkspacesToDisk();
+
// 2. Import all YAML files into the DB so it reflects disk state.
// This populates lastSyncMtime + lastWrittenHash as a side-effect,
// which prevents step 3's watchers from re-importing the same files.
@@ -220,10 +227,83 @@ class RepoFileWatcher {
}
/**
- * Import all YAML files in the repo directory into the DB.
+ * For each workspace linked to this project, if the DB was modified more
+ * recently than the on-disk YAML, write fresh YAML to disk before the
+ * initial `importAllFiles` scan.
+ *
+ * This prevents the stale-YAML-wins problem that occurs when:
+ * 1. User downgrades (old app has no RepoFileWatcher — DB changes aren\u2019t flushed to disk).
+ * 2. User edits requests via the old app (DB updated, no YAML written).
+ * 3. User re-upgrades; without this guard those edits would be silently lost.
*
- * Called during watcher creation and after bulk git operations (clone, pull,
- * merge, checkout) so the DB reflects the current disk state.
+ * Written files are recorded in `lastWrittenHash` / `lastSyncMtime` so that
+ * `importAllFiles` skips them (they are already up-to-date).
+ */
+ private async flushNewerDbWorkspacesToDisk(): Promise {
+ const workspaces = await services.workspace.findByParentId(this.projectId);
+
+ await Promise.all(
+ workspaces.map(async workspace => {
+ try {
+ const meta = await services.workspaceMeta.getByParentId(workspace._id);
+ const gitFilePath = meta?.gitFilePath ?? `insomnia.${workspace._id}.yaml`;
+ const absPath = path.resolve(this.repoDir, gitFilePath);
+
+ // Path-traversal guard
+ const rel = path.relative(this.repoDir, absPath);
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return;
+
+ // Get the most recently modified DB document in this workspace\u2019s tree
+ const allDocs = await db.getWithDescendants(workspace);
+ let maxDbModified: number = workspace.modified ?? 0;
+ for (const doc of allDocs) {
+ const m = (doc as { modified?: number }).modified ?? 0;
+ if (m > maxDbModified) maxDbModified = m;
+ }
+
+ // Compare against the on-disk mtime
+ let fileMtime = 0;
+ try {
+ const stat = await fs.promises.stat(absPath);
+ fileMtime = stat.mtimeMs;
+ } catch {
+ // File doesn\u2019t exist yet \u2014 nothing to do; importAllFiles will handle creation.
+ return;
+ }
+
+ if (maxDbModified <= fileMtime) return; // disk is up-to-date
+
+ // DB is newer \u2014 write fresh YAML so importAllFiles doesn\u2019t overwrite it
+ const yamlContent = await getInsomniaV5DataExport({
+ workspaceId: workspace._id,
+ includePrivateEnvironments: false,
+ });
+ if (!yamlContent?.trim()) return;
+
+ await fs.promises.mkdir(path.dirname(absPath), { recursive: true });
+ await fs.promises.writeFile(absPath, yamlContent, 'utf8');
+
+ const hash = contentHash(yamlContent);
+ const normalised = path.normalize(absPath);
+ this.lastWrittenHash.set(normalised, hash);
+ const newStat = await fs.promises.stat(absPath);
+ this.lastSyncMtime.set(normalised, newStat.mtimeMs);
+
+ console.log(
+ '[repo-file-watcher] DB newer than disk for workspace',
+ workspace._id,
+ '— flushed to',
+ gitFilePath,
+ );
+ } catch (err) {
+ console.warn('[repo-file-watcher] flushNewerDbWorkspacesToDisk error for workspace', workspace._id, err);
+ }
+ }),
+ );
+ }
+
+ /**
+ * Import all YAML files in the repo directory into the DB.
*
* Always bypasses the mtime fast-path (`forceRead`) so every file is read
* and compared by content-hash. This makes the method safe to call at any
@@ -291,6 +371,16 @@ class RepoFileWatcher {
*/
private async flushProjectWorkspacesToDisk(): Promise {
const entries = await this.getWorkspacesWithMeta();
+ const currentWorkspaceIds = new Set(entries.map(({ workspace }) => workspace._id));
+
+ // Find deleted workspaces and remove their files from disk.
+ for (const [workspaceId, absPath] of Array.from(this.lastKnownGitFilePath.entries())) {
+ if (currentWorkspaceIds.has(workspaceId)) {
+ continue;
+ }
+
+ await this.removeWorkspaceFileFromDisk(workspaceId, absPath);
+ }
for (const { workspace, meta } of entries) {
if (this.stopped) {
@@ -327,16 +417,7 @@ class RepoFileWatcher {
// New file written successfully — now safe to remove the old one
if (isRename) {
- try {
- await fs.promises.unlink(previousAbsPath);
- console.log('[repo-file-watcher] Removed old file after rename:', previousAbsPath, '→', absPath);
- } catch {
- // Old file may already be gone — that's fine
- }
- // Clean up tracking for the old path so the watcher doesn't
- // try to re-import a file that no longer exists
- this.lastSyncMtime.delete(previousAbsPath);
- this.lastWrittenHash.delete(previousAbsPath);
+ await this.removeWorkspaceFileFromDisk(workspace._id, previousAbsPath);
}
// Record hash + mtime so the FS→DB side skips this echo
@@ -621,6 +702,37 @@ class RepoFileWatcher {
this.clearProblem(normalised);
}
+ private cleanupRemovedWorkspaceFileTracking(workspaceId: string, normalisedPath: string): void {
+ if (this.lastKnownGitFilePath.get(workspaceId) === normalisedPath) {
+ this.lastKnownGitFilePath.delete(workspaceId);
+ }
+ this.lastSyncMtime.delete(normalisedPath);
+ this.lastWrittenHash.delete(normalisedPath);
+ this.clearProblem(normalisedPath);
+ }
+
+ private async removeWorkspaceFileFromDisk(workspaceId: string, normalisedPath: string): Promise {
+ try {
+ await fs.promises.unlink(normalisedPath);
+ console.log('[repo-file-watcher] Removed workspace file from disk:', workspaceId, normalisedPath);
+ this.cleanupRemovedWorkspaceFileTracking(workspaceId, normalisedPath);
+ } catch (error) {
+ const err = error as NodeJS.ErrnoException;
+ if (err.code === 'ENOENT') {
+ // Old file may already be gone — that's fine.
+ this.cleanupRemovedWorkspaceFileTracking(workspaceId, normalisedPath);
+ return;
+ }
+
+ console.warn(
+ '[repo-file-watcher] Failed to remove workspace file from disk:',
+ workspaceId,
+ normalisedPath,
+ err,
+ );
+ }
+ }
+
/** Convert an absolute path to a posix-style path relative to the repo root. */
private toPosixRelPath(absPath: string): string {
return path.relative(this.repoDir, absPath).split(path.sep).join(path.posix.sep);
@@ -792,6 +904,7 @@ class RepoFileWatcher {
repoId: this.repoId,
problems: this.getProblems(),
workspaceIssues: this.getWorkspaceIssues(),
+ conflictsSuppressed: false,
});
}
}
@@ -900,7 +1013,7 @@ export class RepoFileWatcherRegistry {
}
/** Default notifier that broadcasts to all Electron BrowserWindows. */
-function createElectronNotifier(): WatcherNotifier {
+export function createElectronNotifier(): WatcherNotifier {
return {
onDbSynced: () => {
for (const w of BrowserWindow.getAllWindows()) {
@@ -914,5 +1027,3 @@ function createElectronNotifier(): WatcherNotifier {
},
};
}
-
-export const repoFileWatcherRegistry = new RepoFileWatcherRegistry(createElectronNotifier());
diff --git a/packages/insomnia/src/ui/analytics.ts b/packages/insomnia/src/ui/analytics.ts
index 500699f6c8ba..f2e18440d7c4 100644
--- a/packages/insomnia/src/ui/analytics.ts
+++ b/packages/insomnia/src/ui/analytics.ts
@@ -117,6 +117,8 @@ export enum SegmentEvent {
mcpResponseHeadersCopyAllClicked = 'mcp-response-headers-copy-all-clicked',
kongKonnectPatValidated = 'kong-konnect-pat-validated',
kongKonnectSyncCompleted = 'kong-konnect-sync-completed',
+ emptyStateSendRequestClicked = 'empty-state-send-request-clicked',
+ emptyStateCreateDocumentClicked = 'empty-state-create-document-clicked',
}
type PushPull = 'push' | 'pull';
diff --git a/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx
index f365f628bf71..2bce4ad79c9d 100644
--- a/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx
+++ b/packages/insomnia/src/ui/components/dropdowns/git-project-sync-dropdown.tsx
@@ -459,6 +459,7 @@ export const GitProjectSyncDropdown: FC = ({ gitRepository, activeProject
});
},
onCancelUnresolved: () => {
+ window.main.git.abortMerge({ projectId });
closeGitProjectStagingModalRef.current?.();
setIsPulling(false);
showToast({
diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx
index 07cff3f43f6e..a325f3767052 100644
--- a/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx
+++ b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx
@@ -22,20 +22,24 @@ export const WorkspaceSyncDropdown: FC = () => {
}
const isLocalProject =
- !models.project.isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId && !models.project.isGitProject(activeProject);
+ !models.project.isRemoteProject(activeProject) &&
+ !activeWorkspaceMeta?.gitRepositoryId &&
+ !models.project.isGitProject(activeProject);
if (isLocalProject) {
return ;
}
- const shouldShowCloudSyncDropdown = models.project.isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId;
+ const shouldShowCloudSyncDropdown =
+ models.project.isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId;
if (shouldShowCloudSyncDropdown) {
return ;
}
const shouldShowGitSyncDropdown =
- features.gitSync.enabled && (activeWorkspaceMeta?.gitRepositoryId || !models.project.isRemoteProject(activeProject));
+ features.gitSync.enabled &&
+ (activeWorkspaceMeta?.gitRepositoryId || !models.project.isRemoteProject(activeProject));
if (shouldShowGitSyncDropdown) {
if (models.project.isGitProject(activeProject)) {
return (
diff --git a/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx b/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx
index 9c0fbbeef272..50a1ae5b3d10 100644
--- a/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx
+++ b/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx
@@ -166,7 +166,7 @@ const LocalBranchItem = ({
},
onCancelUnresolved: () => {
// user aborted merge
- window.main.git.abortMerge();
+ window.main.git.abortMerge({ projectId });
// TODO: the abortMerge method provided by isomorphic-git is unreliable
// clean up any partial merges here
reject(
@@ -311,6 +311,7 @@ export const GitProjectBranchesModal: FC = ({ currentBranch, branches, on
}}
isDismissable
className="fixed top-0 left-0 z-10 flex h-(--visual-viewport-height) w-full items-center justify-center bg-black/30"
+ data-testid="git-project-branches-modal-overlay"
>
{
@@ -328,6 +329,7 @@ export const GitProjectBranchesModal: FC = ({ currentBranch, branches, on
diff --git a/packages/insomnia/src/ui/components/modals/git-project-staging-modal.tsx b/packages/insomnia/src/ui/components/modals/git-project-staging-modal.tsx
index fed6d35181f2..e437fb140994 100644
--- a/packages/insomnia/src/ui/components/modals/git-project-staging-modal.tsx
+++ b/packages/insomnia/src/ui/components/modals/git-project-staging-modal.tsx
@@ -813,8 +813,8 @@ const ManualCommitForm: FC = ({
) : null}
-
-
+
+
Staged changes
@@ -840,7 +840,7 @@ const ManualCommitForm: FC = ({
{changes.staged.length}
-
-
+
Unstaged changes
@@ -959,7 +959,7 @@ const ManualCommitForm: FC = ({
-
+
= ({
You can now browse Git Sync project files on your local file system and manage changes using your normal Git
workflows.{' '}
-
+
Learn more ↗
@@ -1371,7 +1371,7 @@ const OriginalGitProjectStagingModal: FC<
)}
-
+
{isGenerateCommitMessagesWithAIEnabled && (
diff --git a/packages/insomnia/src/ui/components/modals/project-modal.tsx b/packages/insomnia/src/ui/components/modals/project-modal.tsx
index c00f1238d421..93adddbbe1a3 100644
--- a/packages/insomnia/src/ui/components/modals/project-modal.tsx
+++ b/packages/insomnia/src/ui/components/modals/project-modal.tsx
@@ -64,6 +64,7 @@ export const ProjectModal = ({
diff --git a/packages/insomnia/src/ui/components/modals/settings-modal.tsx b/packages/insomnia/src/ui/components/modals/settings-modal.tsx
index 37f230ae58f9..84f426bf0857 100644
--- a/packages/insomnia/src/ui/components/modals/settings-modal.tsx
+++ b/packages/insomnia/src/ui/components/modals/settings-modal.tsx
@@ -11,6 +11,7 @@ import { SegmentEvent } from '~/ui/analytics';
import { AISettings } from '~/ui/components/settings/ai-settings';
import { CredentialsSettings } from '~/ui/components/settings/credentials';
import { KonnectSettings } from '~/ui/components/settings/konnect-settings';
+import { ScriptingSettings } from '~/ui/components/settings/scripting-settings';
import { getAppVersion, getProductName } from '../../../common/constants';
import { Modal, type ModalHandle, type ModalProps } from '../base/modal';
@@ -31,7 +32,7 @@ export interface SettingsModalHandle {
show: (options?: { tab?: SettingsModalTabKey }) => void;
}
-type SettingsModalTabKey = 'data' | 'keyboard' | 'themes' | 'plugins' | 'general' | 'proxy' | 'credentials' | 'ai' | 'konnect';
+type SettingsModalTabKey = 'data' | 'keyboard' | 'themes' | 'plugins' | 'general' | 'proxy' | 'credentials' | 'ai' | 'scripting' | 'konnect';
export const SettingsModal = forwardRef((props, ref) => {
const [defaultTabKey, setDefaultTabKey] = useState('general');
@@ -153,6 +154,12 @@ export const SettingsModal = forwardRef((props,
>
Credentials
+
+ Scripting
+
{shouldShowAiSettingsTab && (
((props,
+
+
+
{shouldShowAiSettingsTab && (
diff --git a/packages/insomnia/src/ui/components/project/project-empty-view.tsx b/packages/insomnia/src/ui/components/project/project-empty-view.tsx
index 4acab97561f8..6b151f0a2ebd 100644
--- a/packages/insomnia/src/ui/components/project/project-empty-view.tsx
+++ b/packages/insomnia/src/ui/components/project/project-empty-view.tsx
@@ -23,7 +23,12 @@ export const ProjectEmptyView: FC = ({
{
+ window.main.trackSegmentEvent({
+ event: SegmentEvent.emptyStateSendRequestClicked,
+ });
+ onCreateRequestCollectionWithRequest();
+ }}
>
Send a request
@@ -31,7 +36,12 @@ export const ProjectEmptyView: FC = ({
{
+ window.main.trackSegmentEvent({
+ event: SegmentEvent.emptyStateCreateDocumentClicked,
+ });
+ onCreateDesignDocument();
+ }}
>
Create document
diff --git a/packages/insomnia/src/ui/components/project/project-settings-form.tsx b/packages/insomnia/src/ui/components/project/project-settings-form.tsx
index ae5b9c35ca63..22f5148682db 100644
--- a/packages/insomnia/src/ui/components/project/project-settings-form.tsx
+++ b/packages/insomnia/src/ui/components/project/project-settings-form.tsx
@@ -319,7 +319,7 @@ export const ProjectSettingsForm: FC = ({
diff --git a/packages/insomnia/src/ui/components/settings/credentials.tsx b/packages/insomnia/src/ui/components/settings/credentials.tsx
index e7dfb1e6e129..c35278009737 100644
--- a/packages/insomnia/src/ui/components/settings/credentials.tsx
+++ b/packages/insomnia/src/ui/components/settings/credentials.tsx
@@ -696,7 +696,7 @@ const GitCredentialsList = () => {
export const CredentialsSettings = () => {
return (
-
+
diff --git a/packages/insomnia/src/ui/components/settings/scripting-settings.tsx b/packages/insomnia/src/ui/components/settings/scripting-settings.tsx
new file mode 100644
index 000000000000..96a044ff4aac
--- /dev/null
+++ b/packages/insomnia/src/ui/components/settings/scripting-settings.tsx
@@ -0,0 +1,295 @@
+import { Switch } from 'react-aria-components';
+
+import { useRootLoaderData } from '~/root';
+
+import { type ASTRule, blockedPropertyRules, blockedRootRules, maskRules, type ThreatRule } from '../../../scripting/script-security-policy';
+import { useSettingsPatcher } from '../../hooks/use-request';
+
+const DISABLED_TOOLTIP = 'Enable the script sandbox to configure individual rules';
+
+const RuleToggle = ({
+ name,
+ description,
+ isEnabled,
+ isDisabled,
+ onChange,
+}: {
+ name?: string;
+ description: string;
+ isEnabled: boolean;
+ isDisabled: boolean;
+ onChange: (enabled: boolean) => void;
+}) => (
+
+
+ {name &&
{name} }
+
{description}
+
+
+
+
+
+
+
+ {isDisabled && (
+
+ {DISABLED_TOOLTIP}
+
+ )}
+
+
+);
+
+interface RuleGroup {
+ title: string;
+ description: string;
+ rules: (ThreatRule | ASTRule)[];
+}
+
+const RuleCard = ({
+ title,
+ description,
+ rules,
+ standaloneRules,
+ groups,
+ disabledNames,
+ sandboxEnabled,
+ onToggle,
+}: {
+ title: string;
+ description: string;
+ rules?: (ThreatRule | ASTRule)[];
+ standaloneRules?: (ThreatRule | ASTRule)[];
+ groups?: RuleGroup[];
+ disabledNames: string[];
+ sandboxEnabled: boolean;
+ onToggle: (names: string[], enabled: boolean) => void;
+}) => (
+
+
+
{title}
+
{description}
+
+
+ {rules?.map(rule => (
+
onToggle([rule.name], enabled)}
+ />
+ ))}
+ {standaloneRules && standaloneRules.length > 0 && (
+
+ {standaloneRules.map(rule => (
+ onToggle([rule.name], enabled)}
+ />
+ ))}
+
+ )}
+ {groups?.map(group => {
+ const groupNames = group.rules.map(r => r.name);
+ const allEnabled = groupNames.every(n => !disabledNames.includes(n));
+ return (
+
+
{group.title}
+
onToggle(groupNames, enabled)}
+ />
+
+ {group.rules.map(r => (
+
+ {r.name}
+
+ ))}
+
+
+ );
+ })}
+
+
+);
+
+export const ScriptingSettings = () => {
+ const { settings } = useRootLoaderData()!;
+ const patchSettings = useSettingsPatcher();
+
+ const sandboxEnabled = settings.scriptSandboxEnabled !== false;
+ const strictModeEnabled = settings.scriptStrictModeEnabled !== false;
+ const disabledRules = settings.disabledSecurityRules ?? [];
+ const disabledProperties = settings.disabledBlockedProperties ?? [];
+ const disabledRoots = settings.disabledBlockedRoots ?? [];
+
+ const GROUPED_MASK_NAMES = new Set([
+ 'globalThis', 'global', 'process',
+ 'setImmediate', 'queueMicrotask',
+ 'Proxy', 'Reflect',
+ 'Function', 'WebAssembly',
+ ]);
+
+ const maskRuleGroups: RuleGroup[] = [
+ {
+ title: 'Global & Node.js Internals',
+ description: 'References to the global scope and Node.js process information such as environment variables and runtime state.',
+ rules: maskRules.filter(r => ['globalThis', 'global', 'process'].includes(r.name)),
+ },
+ {
+ title: 'Async Scheduling',
+ description: 'Schedule callbacks to run asynchronously after the current operation completes.',
+ rules: maskRules.filter(r => ['setImmediate', 'queueMicrotask'].includes(r.name)),
+ },
+ {
+ title: 'Runtime APIs',
+ description: 'Used for meta-programming (Proxy, Reflect), creating functions dynamically from strings (Function), and running compiled binary modules (WebAssembly).',
+ rules: maskRules.filter(r => ['Proxy', 'Reflect', 'Function', 'WebAssembly'].includes(r.name)),
+ },
+ ];
+
+ const ungroupedMaskRules = maskRules.filter(r => !GROUPED_MASK_NAMES.has(r.name));
+
+ const STANDALONE_PROPERTY_NAMES = new Set(['mainModule', 'constructor']);
+
+ const GROUPED_PROPERTY_NAMES = new Set([
+ 'prototype', '__proto__', 'getPrototypeOf', 'setPrototypeOf',
+ 'getFunction', 'getThis', 'prepareStackTrace', 'captureStackTrace',
+ '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__',
+ 'defineProperty', 'defineProperties', 'getOwnPropertyDescriptor', 'getOwnPropertyDescriptors',
+ ]);
+
+ const blockedPropertyGroups: RuleGroup[] = [
+ {
+ title: 'Prototype Mutation',
+ description: 'Used to access and modify an object\'s prototype chain.',
+ rules: blockedPropertyRules.filter(r => ['prototype', '__proto__', 'getPrototypeOf', 'setPrototypeOf'].includes(r.name)),
+ },
+ {
+ title: 'Stack Inspection',
+ description: 'Used to inspect and format JavaScript call stack information.',
+ rules: blockedPropertyRules.filter(r => ['prepareStackTrace', 'captureStackTrace', 'getFunction', 'getThis'].includes(r.name)),
+ },
+ {
+ title: 'Accessor Helpers',
+ description: 'Legacy methods for defining and looking up getter and setter functions on objects.',
+ rules: blockedPropertyRules.filter(r => ['__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__', 'defineProperty', 'defineProperties', 'getOwnPropertyDescriptor', 'getOwnPropertyDescriptors'].includes(r.name)),
+ },
+ ];
+
+ const standaloneBlockedPropertyRules = blockedPropertyRules.filter(r => STANDALONE_PROPERTY_NAMES.has(r.name));
+ const ungroupedBlockedPropertyRules = blockedPropertyRules.filter(r => !GROUPED_PROPERTY_NAMES.has(r.name) && !STANDALONE_PROPERTY_NAMES.has(r.name));
+
+ const GROUPED_ROOT_NAMES = new Set([
+ 'globalThis', 'global', 'window', 'self', 'frames',
+ 'process', 'module', 'exports', 'Buffer',
+ 'this', 'constructor', 'arguments',
+ ]);
+
+ const blockedRootGroups: RuleGroup[] = [
+ {
+ title: 'Global Object Aliases',
+ description: 'Different ways to reference the global object depending on the JavaScript environment (browser, Node.js, Web Worker).',
+ rules: blockedRootRules.filter(r => ['globalThis', 'global', 'window', 'self', 'frames'].includes(r.name)),
+ },
+ {
+ title: 'Node.js Internals',
+ description: 'Core Node.js globals for managing the current process, module system, and binary data.',
+ rules: blockedRootRules.filter(r => ['process', 'module', 'exports', 'Buffer'].includes(r.name)),
+ },
+ {
+ title: 'Scopes',
+ description: 'Built-in references to the current execution context, function constructor, and call arguments.',
+ rules: blockedRootRules.filter(r => ['this', 'constructor', 'arguments'].includes(r.name)),
+ },
+ ];
+
+ const ungroupedBlockedRootRules = blockedRootRules.filter(r => !GROUPED_ROOT_NAMES.has(r.name));
+
+ const makeToggler = (field: 'disabledSecurityRules' | 'disabledBlockedProperties' | 'disabledBlockedRoots', current: string[]) =>
+ (names: string[], enabled: boolean) => {
+ const nameSet = new Set(names);
+ const next = enabled ? current.filter(n => !nameSet.has(n)) : [...new Set([...current, ...names])];
+ patchSettings({ [field]: next });
+ };
+
+ return (
+
+
+
+
Script Sandbox
+
+
+
Enable script sandbox
+
+ Pre/post-request scripts run inside a security sandbox that restricts access to dangerous APIs.
+
+
+
patchSettings({ scriptSandboxEnabled: enabled })}
+ className="group flex items-center gap-2"
+ >
+
+
+
+
+
+
+ patchSettings({ scriptStrictModeEnabled: enabled })}
+ />
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/insomnia/src/ui/hooks/use-git-file-issues.ts b/packages/insomnia/src/ui/hooks/use-git-file-issues.ts
index 3fdb898dc343..ab7842125db3 100644
--- a/packages/insomnia/src/ui/hooks/use-git-file-issues.ts
+++ b/packages/insomnia/src/ui/hooks/use-git-file-issues.ts
@@ -19,6 +19,7 @@ const mapIssuesByWorkspaceId = (issues: WorkspaceFileIssue[]) => {
export interface GitFileIssuesValue {
issuesByWorkspaceId: Record
;
+ conflictsSuppressed: boolean;
}
const GitFileIssuesContext = createContext(undefined);
@@ -43,6 +44,7 @@ export const useProjectGitFileIssues = ({
gitRepositoryId?: string | null;
}): GitFileIssuesValue => {
const [issuesByWorkspaceId, setIssuesByWorkspaceId] = useState>({});
+ const [conflictsSuppressed, setConflictsSuppressed] = useState(false);
const loadIssues = useCallback(async () => {
if (!projectId || !gitRepositoryId) {
@@ -76,6 +78,7 @@ export const useProjectGitFileIssues = ({
return;
}
+ setConflictsSuppressed(payload.conflictsSuppressed);
setIssuesByWorkspaceId(mapIssuesByWorkspaceId(payload.workspaceIssues));
});
}, [gitRepositoryId]);
@@ -83,7 +86,8 @@ export const useProjectGitFileIssues = ({
return useMemo(
() => ({
issuesByWorkspaceId,
+ conflictsSuppressed,
}),
- [issuesByWorkspaceId],
+ [issuesByWorkspaceId, conflictsSuppressed],
);
};
diff --git a/packages/insomnia/src/ui/images/git-migration/git.png b/packages/insomnia/src/ui/images/git-migration/git.png
new file mode 100644
index 000000000000..a6f3b23a05c8
Binary files /dev/null and b/packages/insomnia/src/ui/images/git-migration/git.png differ