From 07fff423bb7289438f24189eef6941d9b38cc935 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 13 Jan 2026 16:36:40 +0000 Subject: [PATCH] feat: add `node-vault` util functions (#6796) * feat: add `node-vault` util functions * fix: review comment fixes --- package-lock.json | 434 +---------- packages/bruno-js/package.json | 1 - packages/bruno-requests/src/index.ts | 2 + .../src/utils/node-vault.spec.ts | 716 ++++++++++++++++++ .../bruno-requests/src/utils/node-vault.ts | 326 ++++++++ 5 files changed, 1052 insertions(+), 427 deletions(-) create mode 100644 packages/bruno-requests/src/utils/node-vault.spec.ts create mode 100644 packages/bruno-requests/src/utils/node-vault.ts diff --git a/package-lock.json b/package-lock.json index 1cb567428..eab55e0e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6458,56 +6458,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@postman/form-data": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz", - "integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@postman/tough-cookie": { - "version": "4.1.3-postman.1", - "resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz", - "integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==", - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@postman/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/@postman/tunnel-agent": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.4.tgz", - "integrity": "sha512-CJJlq8V7rNKhAw4sBfjixKpJW00SHqebqNUQKxMoepgeWZIbdPcD+rguRcivGhS4N12PymDcKgUgSD4rVC+RjQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/@prantlf/jsonlint": { "version": "16.0.0", "resolved": "https://registry.npmjs.org/@prantlf/jsonlint/-/jsonlint-16.0.0.tgz", @@ -11429,15 +11379,6 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, "node_modules/asn1.js": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", @@ -11476,6 +11417,7 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "license": "MIT", + "optional": true, "engines": { "node": ">=0.8" } @@ -11626,15 +11568,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/aws4": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", @@ -12028,15 +11961,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -12229,15 +12153,6 @@ "dev": true, "license": "MIT" }, - "node_modules/brotli": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", - "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", - "license": "MIT", - "dependencies": { - "base64-js": "^1.1.2" - } - }, "node_modules/browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -12750,12 +12665,6 @@ "node": ">=4" } }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "license": "Apache-2.0" - }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -14375,18 +14284,6 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -15110,22 +15007,6 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "license": "MIT", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/ecc-jsbn/node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -16424,12 +16305,6 @@ "node": ">= 0.6" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, "node_modules/extract-files": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", @@ -16555,6 +16430,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "devOptional": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -17037,15 +16913,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", @@ -17524,15 +17391,6 @@ "node": ">=6.0" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/github-markdown-css": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.8.1.tgz", @@ -17815,57 +17673,12 @@ "@grpc/grpc-js": "^1.12.6" } }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "license": "ISC", - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "license": "MIT", - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/har-validator-compiled": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/har-validator-compiled/-/har-validator-compiled-1.0.0.tgz", "integrity": "sha512-dher7nFSx+Ef6OoqVveLClh8itAR3vd8Qx70Lh/hEgP1iGeARAolbci7Y8JBrHIYgFCT6xRdvvL16AR9Zh07Dw==", "license": "MIT" }, - "node_modules/har-validator/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/har-validator/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -18338,20 +18151,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/http-signature": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", - "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^2.0.2", - "sshpk": "^1.14.1" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -19168,12 +18967,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "license": "MIT" - }, "node_modules/is-valid-path": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz", @@ -19236,12 +19029,6 @@ "node": ">=0.10.0" } }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "license": "MIT" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -20822,12 +20609,6 @@ "node": "*" } }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -20851,7 +20632,9 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC" + "dev": true, + "license": "ISC", + "optional": true }, "node_modules/json5": { "version": "2.2.3", @@ -20892,44 +20675,6 @@ "node": "*" } }, - "node_modules/jsprim": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", - "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, - "node_modules/jsprim/node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/jsprim/node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -22030,15 +21775,6 @@ "node": ">= 6.0.0" } }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - } - }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -22259,44 +21995,6 @@ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "license": "MIT" }, - "node_modules/node-vault": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/node-vault/-/node-vault-0.10.2.tgz", - "integrity": "sha512-//uc9/YImE7Dx0QHdwMiAzLaOumiKUnOUP8DymgtkZ8nsq6/V2LKvEu6kw91Lcruw8lWUfj4DO7CIXNPRWBuuA==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "mustache": "^4.2.0", - "postman-request": "^2.88.1-postman.33", - "tv4": "^1.3.0" - }, - "engines": { - "node": ">= 16.0.0" - } - }, - "node_modules/node-vault/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/node-vault/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -22367,15 +22065,6 @@ "dev": true, "license": "MIT" }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "license": "Apache-2.0", - "engines": { - "node": "*" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -23090,12 +22779,6 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -24075,57 +23758,6 @@ "node": ">=15.0.0" } }, - "node_modules/postman-request": { - "version": "2.88.1-postman.40", - "resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.40.tgz", - "integrity": "sha512-uE4AiIqhjtHKp4pj9ei7fkdfNXEX9IqDBlK1plGAQne6y79UUlrTdtYLhwXoO0AMOvqyl9Ar+BU6Eo6P/MPgfg==", - "license": "Apache-2.0", - "dependencies": { - "@postman/form-data": "~3.1.1", - "@postman/tough-cookie": "~4.1.3-postman.1", - "@postman/tunnel-agent": "^0.6.4", - "aws-sign2": "~0.7.0", - "aws4": "^1.12.0", - "brotli": "^1.3.3", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "har-validator": "~5.1.3", - "http-signature": "~1.3.1", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "^2.1.35", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.3", - "safe-buffer": "^5.1.2", - "stream-length": "^1.0.2", - "uuid": "^8.3.2" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/postman-request/node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/postman-request/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -24427,6 +24059,7 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -24471,6 +24104,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -27386,37 +27020,6 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", - "license": "MIT", - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sshpk/node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "license": "MIT" - }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -27587,21 +27190,6 @@ "node": ">= 6" } }, - "node_modules/stream-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz", - "integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==", - "license": "WTFPL", - "dependencies": { - "bluebird": "^2.6.2" - } - }, - "node_modules/stream-length/node_modules/bluebird": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==", - "license": "MIT" - }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -29184,12 +28772,6 @@ "node": ">= 0.8.0" } }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -29454,6 +29036,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -35492,7 +35075,6 @@ "moment": "^2.29.4", "nanoid": "3.3.8", "node-fetch": "^2.7.0", - "node-vault": "^0.10.2", "path": "^0.12.7", "quickjs-emscripten": "^0.29.2", "tv4": "^1.3.0", diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json index c1a6eebde..96af47263 100644 --- a/packages/bruno-js/package.json +++ b/packages/bruno-js/package.json @@ -29,7 +29,6 @@ "moment": "^2.29.4", "nanoid": "3.3.8", "node-fetch": "^2.7.0", - "node-vault": "^0.10.2", "path": "^0.12.7", "quickjs-emscripten": "^0.29.2", "tv4": "^1.3.0", diff --git a/packages/bruno-requests/src/index.ts b/packages/bruno-requests/src/index.ts index ad751f0ce..52ef41696 100644 --- a/packages/bruno-requests/src/index.ts +++ b/packages/bruno-requests/src/index.ts @@ -4,5 +4,7 @@ export { WsClient } from './ws/ws-client'; export { default as cookies } from './cookies'; export { getCACertificates } from './utils/ca-cert'; +export { default as createVaultClient, VaultError } from './utils/node-vault'; +export type { VaultClient, VaultConfig, VaultRequestOptions } from './utils/node-vault'; export * as scripting from './scripting'; diff --git a/packages/bruno-requests/src/utils/node-vault.spec.ts b/packages/bruno-requests/src/utils/node-vault.spec.ts new file mode 100644 index 000000000..8a0ee7daa --- /dev/null +++ b/packages/bruno-requests/src/utils/node-vault.spec.ts @@ -0,0 +1,716 @@ +import axios from 'axios'; +import createVaultClient, { VaultError, VaultClient } from './node-vault'; + +// Mock axios +jest.mock('axios', () => { + const mockAxios = jest.fn(); + (mockAxios as any).isAxiosError = jest.fn((error: any) => error.isAxiosError === true); + return mockAxios; +}); + +const mockedAxios = axios as jest.MockedFunction; + +describe('node-vault', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Clear environment variables + delete process.env.VAULT_ADDR; + delete process.env.VAULT_TOKEN; + delete process.env.VAULT_NAMESPACE; + }); + + describe('module', () => { + it('should export a function that returns a new client', () => { + const vault = createVaultClient(); + expect(typeof createVaultClient).toBe('function'); + expect(typeof vault).toBe('object'); + }); + + it('should set default values for endpoint and apiVersion', () => { + const vault = createVaultClient(); + expect(vault.endpoint).toBe('http://127.0.0.1:8200'); + expect(vault.apiVersion).toBe('v1'); + }); + + it('should use environment variables for defaults', () => { + process.env.VAULT_ADDR = 'https://vault.example.com'; + process.env.VAULT_TOKEN = 'env-token'; + process.env.VAULT_NAMESPACE = 'env-namespace'; + + const vault = createVaultClient(); + expect(vault.endpoint).toBe('https://vault.example.com'); + expect(vault.token).toBe('env-token'); + expect(vault.namespace).toBe('env-namespace'); + }); + + it('should allow config to override environment variables', () => { + process.env.VAULT_ADDR = 'https://vault.example.com'; + process.env.VAULT_TOKEN = 'env-token'; + + const vault = createVaultClient({ + endpoint: 'https://custom.vault.com', + token: 'config-token' + }); + expect(vault.endpoint).toBe('https://custom.vault.com'); + expect(vault.token).toBe('config-token'); + }); + }); + + describe('client properties', () => { + it('should allow direct assignment of endpoint', () => { + const vault = createVaultClient(); + vault.endpoint = 'https://new-vault.example.com'; + expect(vault.endpoint).toBe('https://new-vault.example.com'); + }); + + it('should allow direct assignment of token', () => { + const vault = createVaultClient(); + vault.token = 'new-token'; + expect(vault.token).toBe('new-token'); + }); + + it('should allow direct assignment of namespace', () => { + const vault = createVaultClient(); + vault.namespace = 'my-namespace'; + expect(vault.namespace).toBe('my-namespace'); + }); + + it('should allow direct assignment of apiVersion', () => { + const vault = createVaultClient(); + vault.apiVersion = 'v2'; + expect(vault.apiVersion).toBe('v2'); + }); + }); + + describe('read(path, requestOptions)', () => { + let vault: VaultClient; + + beforeEach(() => { + vault = createVaultClient({ + endpoint: 'http://localhost:8200', + token: 'test-token' + }); + }); + + it('should read data from path', async () => { + const responseData = { data: { value: 'secret-value' } }; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: responseData + }); + + const result = await vault.read('secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledTimes(1); + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'GET', + url: 'http://localhost:8200/v1/secret/data/hello', + headers: expect.objectContaining({ + 'X-Vault-Token': 'test-token' + }) + }) + ); + expect(result).toEqual(responseData); + }); + + it('should include namespace header when set', async () => { + vault.namespace = 'my-namespace'; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: { data: {} } + }); + + await vault.read('secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Vault-Token': 'test-token', + 'X-Vault-Namespace': 'my-namespace' + }) + }) + ); + }); + + it('should use updated endpoint after assignment', async () => { + vault.endpoint = 'https://new-vault.com'; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: { data: {} } + }); + + await vault.read('secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'https://new-vault.com/v1/secret/data/hello' + }) + ); + }); + + it('should handle 404 errors', async () => { + mockedAxios.mockResolvedValueOnce({ + status: 404, + data: { errors: ['no secrets found'] } + }); + + await expect(vault.read('secret/data/nonexistent')).rejects.toThrow('no secrets found'); + }); + + it('should handle 204 no content response', async () => { + mockedAxios.mockResolvedValueOnce({ + status: 204, + data: null + }); + + const result = await vault.read('secret/data/empty'); + expect(result).toBeNull(); + }); + + it('should handle paths with leading slash without creating double slashes', async () => { + const responseData = { data: { value: 'secret-value' } }; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: responseData + }); + + const result = await vault.read('/secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledTimes(1); + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'GET', + url: 'http://localhost:8200/v1/secret/data/hello', + headers: expect.objectContaining({ + 'X-Vault-Token': 'test-token' + }) + }) + ); + expect(result).toEqual(responseData); + }); + + it('should handle endpoint with trailing slash', async () => { + vault.endpoint = 'http://localhost:8200/'; + const responseData = { data: { value: 'secret-value' } }; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: responseData + }); + + const result = await vault.read('secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledTimes(1); + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'GET', + url: 'http://localhost:8200/v1/secret/data/hello' + }) + ); + expect(result).toEqual(responseData); + }); + }); + + describe('write(path, data, requestOptions)', () => { + let vault: VaultClient; + + beforeEach(() => { + vault = createVaultClient({ + endpoint: 'http://localhost:8200', + token: 'test-token' + }); + }); + + it('should write data to path', async () => { + const writeData = { value: 'world' }; + const responseData = { data: { created_time: '2024-01-01T00:00:00Z' } }; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: responseData + }); + + const result = await vault.write('secret/data/hello', writeData); + + expect(mockedAxios).toHaveBeenCalledTimes(1); + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + url: 'http://localhost:8200/v1/secret/data/hello', + data: writeData, + headers: expect.objectContaining({ + 'X-Vault-Token': 'test-token', + 'Content-Type': 'application/json' + }) + }) + ); + expect(result).toEqual(responseData); + }); + + it('should handle LDAP login write', async () => { + const loginData = { password: 'my-password' }; + const responseData = { + auth: { + client_token: 'ldap-token', + renewable: true, + lease_duration: 3600 + } + }; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: responseData + }); + + const result = await vault.write('auth/ldap/login/myuser', loginData); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + url: 'http://localhost:8200/v1/auth/ldap/login/myuser', + data: loginData + }) + ); + expect(result.auth.client_token).toBe('ldap-token'); + }); + }); + + describe('approleLogin(args)', () => { + let vault: VaultClient; + + beforeEach(() => { + vault = createVaultClient({ + endpoint: 'http://localhost:8200' + }); + }); + + it('should login with role_id and secret_id', async () => { + const responseData = { + auth: { + client_token: 'approle-token', + renewable: true, + lease_duration: 3600, + policies: ['default', 'my-policy'] + } + }; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: responseData + }); + + const result = await vault.approleLogin({ + role_id: 'my-role-id', + secret_id: 'my-secret-id' + }); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + url: 'http://localhost:8200/v1/auth/approle/login', + data: { + role_id: 'my-role-id', + secret_id: 'my-secret-id' + } + }) + ); + expect(result.auth.client_token).toBe('approle-token'); + }); + + it('should login with only role_id when secret_id is not required', async () => { + const responseData = { + auth: { client_token: 'approle-token' } + }; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: responseData + }); + + await vault.approleLogin({ + role_id: 'my-role-id' + }); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + data: { + role_id: 'my-role-id' + } + }) + ); + }); + + it('should use custom mount_point', async () => { + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: { auth: { client_token: 'token' } } + }); + + await vault.approleLogin({ + role_id: 'my-role-id', + secret_id: 'my-secret-id', + mount_point: 'custom-approle' + }); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8200/v1/auth/custom-approle/login' + }) + ); + }); + + it('should handle authentication errors', async () => { + mockedAxios.mockResolvedValueOnce({ + status: 400, + data: { errors: ['invalid role or secret ID'] } + }); + + await expect(vault.approleLogin({ + role_id: 'bad-role-id', + secret_id: 'bad-secret-id' + })).rejects.toThrow('invalid role or secret ID'); + }); + }); + + describe('tokenLookupSelf()', () => { + let vault: VaultClient; + + beforeEach(() => { + vault = createVaultClient({ + endpoint: 'http://localhost:8200', + token: 'my-token' + }); + }); + + it('should lookup current token', async () => { + const responseData = { + data: { + id: 'my-token', + ttl: 3600, + renewable: true, + policies: ['default'] + } + }; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: responseData + }); + + const result = await vault.tokenLookupSelf(); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'GET', + url: 'http://localhost:8200/v1/auth/token/lookup-self', + headers: expect.objectContaining({ + 'X-Vault-Token': 'my-token' + }) + }) + ); + expect(result.data.ttl).toBe(3600); + }); + + it('should handle expired token error', async () => { + mockedAxios.mockResolvedValueOnce({ + status: 403, + data: { errors: ['permission denied'] } + }); + + await expect(vault.tokenLookupSelf()).rejects.toThrow('permission denied'); + }); + }); + + describe('tokenRenewSelf(args)', () => { + let vault: VaultClient; + + beforeEach(() => { + vault = createVaultClient({ + endpoint: 'http://localhost:8200', + token: 'my-token' + }); + }); + + it('should renew current token', async () => { + const responseData = { + auth: { + client_token: 'my-token', + renewable: true, + lease_duration: 7200 + } + }; + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: responseData + }); + + const result = await vault.tokenRenewSelf(); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + url: 'http://localhost:8200/v1/auth/token/renew-self', + headers: expect.objectContaining({ + 'X-Vault-Token': 'my-token' + }) + }) + ); + expect(result.auth.lease_duration).toBe(7200); + }); + + it('should pass increment when provided', async () => { + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: { auth: { lease_duration: 3600 } } + }); + + await vault.tokenRenewSelf({ increment: 3600 }); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + data: { increment: 3600 } + }) + ); + }); + + it('should handle non-renewable token error', async () => { + mockedAxios.mockResolvedValueOnce({ + status: 400, + data: { errors: ['lease is not renewable'] } + }); + + await expect(vault.tokenRenewSelf()).rejects.toThrow('lease is not renewable'); + }); + }); + + describe('error handling', () => { + let vault: VaultClient; + + beforeEach(() => { + vault = createVaultClient({ + endpoint: 'http://localhost:8200', + token: 'test-token' + }); + }); + + it('should throw VaultError with response structure', async () => { + mockedAxios.mockResolvedValueOnce({ + status: 500, + data: { errors: ['internal server error'] } + }); + + try { + await vault.read('secret/data/hello'); + } catch (error) { + expect(error).toBeInstanceOf(VaultError); + expect((error as VaultError).message).toBe('internal server error'); + expect((error as VaultError).response).toEqual({ + statusCode: 500, + status: 500, + body: { errors: ['internal server error'] } + }); + } + }); + + it('should handle error without errors array', async () => { + mockedAxios.mockResolvedValueOnce({ + status: 503, + data: {} + }); + + await expect(vault.read('secret/data/hello')).rejects.toThrow('Status 503'); + }); + + it('should handle network errors', async () => { + const networkError = new Error('Network Error'); + (networkError as any).isAxiosError = true; + (networkError as any).code = 'ECONNREFUSED'; + mockedAxios.mockRejectedValueOnce(networkError); + + try { + await vault.read('secret/data/hello'); + } catch (error) { + expect((error as any).message).toBe('Network Error'); + expect((error as any).code).toBe('ECONNREFUSED'); + } + }); + + it('should handle axios error with response', async () => { + const axiosError = new Error('Request failed'); + (axiosError as any).isAxiosError = true; + (axiosError as any).response = { + status: 401, + data: { errors: ['permission denied'] } + }; + mockedAxios.mockRejectedValueOnce(axiosError); + + await expect(vault.read('secret/data/hello')).rejects.toThrow('permission denied'); + }); + + it('should pass through non-axios errors', async () => { + const genericError = new Error('Unknown error'); + mockedAxios.mockRejectedValueOnce(genericError); + + await expect(vault.read('secret/data/hello')).rejects.toThrow('Unknown error'); + }); + }); + + describe('requestOptions', () => { + it('should pass strictSSL to https agent', async () => { + const vault = createVaultClient({ + endpoint: 'https://vault.example.com', + token: 'test-token', + requestOptions: { + strictSSL: false + } + }); + + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: {} + }); + + await vault.read('secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + httpsAgent: expect.any(Object) + }) + ); + }); + + it('should not set httpsAgent for http endpoints', async () => { + const vault = createVaultClient({ + endpoint: 'http://localhost:8200', + token: 'test-token', + requestOptions: { + strictSSL: false + } + }); + + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: {} + }); + + await vault.read('secret/data/hello'); + + const callArgs = mockedAxios.mock.calls[0][0] as any; + expect(callArgs.httpsAgent).toBeUndefined(); + }); + + it('should configure proxy when provided', async () => { + const vault = createVaultClient({ + endpoint: 'http://localhost:8200', + token: 'test-token', + requestOptions: { + proxy: 'http://proxy.example.com:8080' + } + }); + + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: {} + }); + + await vault.read('secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + proxy: expect.objectContaining({ + host: 'proxy.example.com', + port: 8080 + }) + }) + ); + }); + + it('should configure proxy with authentication', async () => { + const vault = createVaultClient({ + endpoint: 'http://localhost:8200', + token: 'test-token', + requestOptions: { + proxy: 'http://user:pass@proxy.example.com:8080' + } + }); + + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: {} + }); + + await vault.read('secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + proxy: expect.objectContaining({ + host: 'proxy.example.com', + port: 8080, + auth: { + username: 'user', + password: 'pass' + } + }) + }) + ); + }); + }); + + describe('URL construction', () => { + it('should construct URL with apiVersion', async () => { + const vault = createVaultClient({ + endpoint: 'http://localhost:8200', + apiVersion: 'v2' + }); + + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: {} + }); + + await vault.read('secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8200/v2/secret/data/hello' + }) + ); + }); + + it('should handle endpoint without trailing slash', async () => { + const vault = createVaultClient({ + endpoint: 'http://localhost:8200' + }); + + mockedAxios.mockResolvedValueOnce({ + status: 200, + data: {} + }); + + await vault.read('secret/data/hello'); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + url: 'http://localhost:8200/v1/secret/data/hello' + }) + ); + }); + }); + + describe('health endpoint handling', () => { + it('should not throw error for sys/health even with non-200 status', async () => { + const vault = createVaultClient({ + endpoint: 'http://localhost:8200' + }); + + const healthResponse = { + initialized: true, + sealed: true, + standby: true + }; + + mockedAxios.mockResolvedValueOnce({ + status: 503, + data: healthResponse + }); + + const result = await vault.read('sys/health'); + expect(result).toEqual(healthResponse); + }); + }); +}); diff --git a/packages/bruno-requests/src/utils/node-vault.ts b/packages/bruno-requests/src/utils/node-vault.ts new file mode 100644 index 000000000..b4e2d31e9 --- /dev/null +++ b/packages/bruno-requests/src/utils/node-vault.ts @@ -0,0 +1,326 @@ +import axios, { AxiosRequestConfig, AxiosError } from 'axios'; +import * as https from 'node:https'; + +/** + * Configuration options for creating a Vault client + */ +export interface VaultConfig { + apiVersion?: string; + endpoint?: string; + token?: string; + namespace?: string; + requestOptions?: VaultRequestOptions; + debug?: (...args: any[]) => void; +} + +/** + * Request options for Vault HTTP requests + * Compatible with node-vault's requestOptions + */ +export interface VaultRequestOptions { + strictSSL?: boolean; + ca?: string | Buffer | Array; + proxy?: string; + [key: string]: any; +} + +/** + * AppRole login arguments + */ +export interface ApproleLoginArgs { + role?: string; + role_id: string; + secret_id?: string; + mount_point?: string; +} + +/** + * Token renew arguments + */ +export interface TokenRenewArgs { + increment?: number | string; +} + +/** + * Vault API response error structure + * Includes both statusCode (node-vault style) and status (axios style) for compatibility + */ +export class VaultError extends Error { + response?: { + statusCode: number; + status: number; // Alias for axios-style error handling + body: any; + }; + + code?: string; // For network errors + + constructor(message: string, response?: { statusCode: number; body: any }) { + super(message); + this.name = 'VaultError'; + if (response) { + this.response = { + statusCode: response.statusCode, + status: response.statusCode, // Alias for compatibility + body: response.body + }; + } + } +} + +/** + * Vault client interface - matches node-vault API surface + */ +export interface VaultClient { + endpoint: string; + namespace?: string; + token?: string; + apiVersion: string; + + read(path: string, requestOptions?: VaultRequestOptions): Promise; + write(path: string, data: any, requestOptions?: VaultRequestOptions): Promise; + approleLogin(args: ApproleLoginArgs): Promise; + tokenLookupSelf(args?: any): Promise; + tokenRenewSelf(args?: TokenRenewArgs): Promise; +} + +/** + * Creates an HTTPS agent based on request options + */ +function createHttpsAgent(options: VaultRequestOptions): https.Agent | undefined { + const agentOptions: https.AgentOptions = {}; + let needsAgent = false; + + if (options.strictSSL === false) { + agentOptions.rejectUnauthorized = false; + needsAgent = true; + } + + if (options.ca) { + agentOptions.ca = options.ca; + needsAgent = true; + } + + return needsAgent ? new https.Agent(agentOptions) : undefined; +} + +/** + * Handles Vault API response, extracting body or throwing error + */ +function handleVaultResponse(statusCode: number, body: any, path: string): any { + // Success responses + if (statusCode === 200 || statusCode === 204) { + return body; + } + + // Health endpoint special handling (matches node-vault behavior) + if (path.match(/sys\/health/) !== null) { + return body; + } + + // Error responses + let message: string; + if (body && body.errors && body.errors.length > 0) { + message = body.errors[0]; + } else { + message = `Status ${statusCode}`; + } + + throw new VaultError(message, { statusCode, body }); +} + +/** + * Creates a Vault client instance + * + * This is a drop-in replacement for node-vault, implementing only the methods + * used by bruno-electron and bruno-cli. + * + * @param config - Configuration options + * @returns VaultClient instance with mutable properties + * + * @example + * ```javascript + * const vault = createVaultClient({ apiVersion: 'v1' }); + * vault.endpoint = 'https://vault.example.com'; + * vault.token = 'my-token'; + * const secret = await vault.read('secret/data/myapp'); + * ``` + */ +function createVaultClient(config: VaultConfig = {}): VaultClient { + const debug = config.debug || (() => {}); + const defaultRequestOptions = config.requestOptions || {}; + + /** + * Makes an HTTP request to the Vault API + */ + async function request( + method: string, + path: string, + data?: any, + requestOptions?: VaultRequestOptions + ): Promise { + // Merge request options: defaults from config + per-request options + const mergedOptions: VaultRequestOptions = { + ...defaultRequestOptions, + ...requestOptions + }; + + const endpointOrigin = client.endpoint?.endsWith('/') ? client.endpoint : `${client.endpoint}/`; + + // Build URL + const uri = `${endpointOrigin}${client.apiVersion}${path}`; + debug(method, uri); + + // Build headers + const headers: Record = { + 'Content-Type': 'application/json' + }; + + if (typeof client.token === 'string' && client.token.length) { + headers['X-Vault-Token'] = client.token; + } + + if (typeof client.namespace === 'string' && client.namespace.length) { + headers['X-Vault-Namespace'] = client.namespace; + } + + // Build axios config + const axiosConfig: AxiosRequestConfig = { + method: method as any, + url: uri, + headers, + validateStatus: () => true // Don't throw on non-2xx status + }; + + // Add request body for POST/PUT + if (data && (method === 'POST' || method === 'PUT')) { + axiosConfig.data = data; + debug('data:', data); + } + + // Configure HTTPS agent + if (uri.startsWith('https')) { + const agent = createHttpsAgent(mergedOptions); + if (agent) { + axiosConfig.httpsAgent = agent; + } + } + + // Configure proxy + if (mergedOptions.proxy) { + // Parse proxy URL into axios proxy config + try { + const proxyUrl = new URL(mergedOptions.proxy); + axiosConfig.proxy = { + host: proxyUrl.hostname, + port: parseInt(proxyUrl.port, 10) || (proxyUrl.protocol === 'https:' ? 443 : 80), + protocol: proxyUrl.protocol.replace(':', '') + }; + if (proxyUrl.username && proxyUrl.password) { + axiosConfig.proxy.auth = { + username: decodeURIComponent(proxyUrl.username), + password: decodeURIComponent(proxyUrl.password) + }; + } + } catch (e) { + // If proxy URL parsing fails, pass it as-is for backward compatibility + debug('Failed to parse proxy URL:', mergedOptions.proxy); + } + } + + try { + const response = await axios(axiosConfig); + return handleVaultResponse(response.status, response.data, path); + } catch (error) { + // Network errors or other axios errors + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + if (axiosError.response) { + // Server responded with error status + return handleVaultResponse( + axiosError.response.status, + axiosError.response.data, + path + ); + } + // Network error - preserve original error structure + const vaultError = new VaultError(axiosError.message); + (vaultError as any).code = axiosError.code; + throw vaultError; + } + throw error; + } + } + + // Create client object with mutable properties + const client: VaultClient = { + // Mutable properties (support direct assignment like node-vault) + apiVersion: config.apiVersion || 'v1', + endpoint: config.endpoint || process.env.VAULT_ADDR || 'http://127.0.0.1:8200', + token: config.token || process.env.VAULT_TOKEN, + namespace: config.namespace || process.env.VAULT_NAMESPACE, + + /** + * Read data from a Vault path + * @param path - The path to read from (e.g., 'secret/data/myapp') + * @param requestOptions - Optional request options + */ + async read(path: string, requestOptions?: VaultRequestOptions): Promise { + path = path.startsWith('/') ? path : `/${path}`; + debug('read', path); + return request('GET', path, undefined, requestOptions); + }, + + /** + * Write data to a Vault path + * @param path - The path to write to + * @param data - The data to write + * @param requestOptions - Optional request options + */ + async write(path: string, data: any, requestOptions?: VaultRequestOptions): Promise { + path = path.startsWith('/') ? path : `/${path}`; + debug('write', path, data); + return request('POST', path, data, requestOptions); + }, + + /** + * Authenticate using AppRole + * @param args - AppRole login arguments + */ + async approleLogin(args: ApproleLoginArgs): Promise { + debug('approleLogin', args.role_id); + const mountPoint = args.mount_point || 'approle'; + const body: Record = { + role_id: args.role_id + }; + if (args.secret_id) { + body.secret_id = args.secret_id; + } + return request('POST', `/auth/${mountPoint}/login`, body); + }, + + /** + * Look up the current token's properties + */ + async tokenLookupSelf(args?: any): Promise { + debug('tokenLookupSelf'); + return request('GET', '/auth/token/lookup-self'); + }, + + /** + * Renew the current token + * @param args - Optional arguments including increment + */ + async tokenRenewSelf(args?: TokenRenewArgs): Promise { + debug('tokenRenewSelf'); + const body: Record = {}; + if (args?.increment !== undefined) { + body.increment = args.increment; + } + return request('POST', '/auth/token/renew-self', Object.keys(body).length > 0 ? body : undefined); + } + }; + + return client; +} + +export default createVaultClient;