From c88b1a624fc3832b15b884896b22375f976ee45e Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:06:06 +0100 Subject: [PATCH 01/20] Type check safe-chain package --- package-lock.json | 357 ++++++++++++++++++ packages/safe-chain-bun/src/index.js | 2 +- packages/safe-chain/bin/aikido-bun.js | 1 + packages/safe-chain/bin/aikido-bunx.js | 1 + packages/safe-chain/bin/aikido-npm.js | 1 + packages/safe-chain/bin/aikido-npx.js | 1 + packages/safe-chain/bin/aikido-pnpm.js | 1 + packages/safe-chain/bin/aikido-pnpx.js | 1 + packages/safe-chain/bin/aikido-yarn.js | 1 + packages/safe-chain/package.json | 11 +- packages/safe-chain/src/api/aikido.js | 15 +- packages/safe-chain/src/api/npmApi.js | 25 +- .../safe-chain/src/config/cliArguments.js | 16 + packages/safe-chain/src/config/configFile.js | 51 ++- .../src/environment/userInteraction.js | 28 ++ packages/safe-chain/src/main.js | 8 +- .../packagemanager/_shared/matchesCommand.js | 5 + .../bun/createBunPackageManager.js | 18 +- .../packagemanager/currentPackageManager.js | 15 + .../npm/createPackageManager.js | 23 ++ .../commandArgumentScanner.js | 37 ++ .../npm/dependencyScanner/nullScanner.js | 5 +- .../parsing/parsePackagesFromInstallArgs.js | 37 +- .../src/packagemanager/npm/runNpmCommand.js | 15 +- .../npm/utils/abbrevs-generated.js | 1 + .../src/packagemanager/npm/utils/cmd-list.js | 5 + .../packagemanager/npm/utils/npmCommands.js | 8 + .../npx/createPackageManager.js | 3 + .../commandArgumentScanner.js | 14 +- .../npx/parsing/parsePackagesFromArguments.js | 22 ++ .../src/packagemanager/npx/runNpxCommand.js | 8 +- .../pnpm/createPackageManager.js | 11 + .../commandArgumentScanner.js | 7 + .../parsing/parsePackagesFromArguments.js | 21 ++ .../src/packagemanager/pnpm/runPnpmCommand.js | 9 +- .../yarn/createPackageManager.js | 8 + .../commandArgumentScanner.js | 7 + .../parsing/parsePackagesFromArguments.js | 24 ++ .../src/packagemanager/yarn/runYarnCommand.js | 13 +- .../safe-chain/src/registryProxy/certUtils.js | 4 + .../src/registryProxy/mitmRequestHandler.js | 40 +- .../src/registryProxy/parsePackageFromUrl.js | 4 + .../src/registryProxy/plainHttpProxy.js | 9 + .../src/registryProxy/registryProxy.js | 33 ++ .../src/registryProxy/tunnelRequestHandler.js | 22 ++ .../safe-chain/src/scanning/audit/index.js | 23 ++ packages/safe-chain/src/scanning/index.js | 19 +- .../src/scanning/malwareDatabase.js | 25 +- .../src/shell-integration/helpers.js | 41 +- .../src/shell-integration/setup-ci.js | 20 + .../safe-chain/src/shell-integration/setup.js | 5 +- .../src/shell-integration/shellDetection.js | 13 +- .../supported-shells/bash.js | 20 +- .../supported-shells/fish.js | 10 +- .../supported-shells/powershell.js | 10 +- .../supported-shells/windowsPowershell.js | 10 +- .../shell-integration/supported-shells/zsh.js | 7 +- .../src/shell-integration/teardown.js | 5 +- packages/safe-chain/src/utils/safeSpawn.js | 35 ++ packages/safe-chain/tsconfig.json | 21 ++ 60 files changed, 1179 insertions(+), 33 deletions(-) create mode 100644 packages/safe-chain/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 88e9fb5..4b0b5cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -411,6 +411,94 @@ "node": ">=14" } }, + "node_modules/@types/make-fetch-happen": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@types/make-fetch-happen/-/make-fetch-happen-10.0.4.tgz", + "integrity": "sha512-jKzweQaEMMAi55ehvR1z0JF6aSVQm/h1BXBhPLOJriaeQBctjw5YbpIGs7zAx9dN0Sa2OO5bcXwCkrlgenoPEA==", + "dev": true, + "dependencies": { + "@types/node-fetch": "*", + "@types/retry": "*", + "@types/ssri": "*" + } + }, + "node_modules/@types/node": { + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "dev": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/npm-package-arg": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/npm-package-arg/-/npm-package-arg-6.1.4.tgz", + "integrity": "sha512-vDgdbMy2QXHnAruzlv68pUtXCjmqUk3WrBAsRboRovsOmxbfn/WiYCjmecyKjGztnMps5dWp4Uq2prp+Ilo17Q==", + "dev": true + }, + "node_modules/@types/npm-registry-fetch": { + "version": "8.0.9", + "resolved": "https://registry.npmjs.org/@types/npm-registry-fetch/-/npm-registry-fetch-8.0.9.tgz", + "integrity": "sha512-7NxvodR5Yrop3pb6+n8jhJNyzwOX0+6F+iagNEoi9u1CGxruYAwZD8pvGc9prIkL0+FdX5Xp0p80J9QPrGUp/g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/node-fetch": "*", + "@types/npm-package-arg": "*", + "@types/npmlog": "*", + "@types/ssri": "*" + } + }, + "node_modules/@types/npmlog": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@types/npmlog/-/npmlog-7.0.0.tgz", + "integrity": "sha512-hJWbrKFvxKyWwSUXjZMYTINsSOY6IclhvGOZ97M8ac2tmR9hMwmTnYaMdpGhvju9ctWLTPhCS+eLfQNluiEjQQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true + }, + "node_modules/@types/ssri": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@types/ssri/-/ssri-7.1.5.tgz", + "integrity": "sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -444,6 +532,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -516,6 +610,19 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/chalk": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", @@ -581,6 +688,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -612,6 +731,29 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -653,6 +795,51 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -669,6 +856,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-minipass": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", @@ -681,6 +884,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-east-asian-width": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", @@ -693,6 +905,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -728,6 +977,57 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hosted-git-info": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", @@ -919,6 +1219,36 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -1563,6 +1893,25 @@ "node": ">=18" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true + }, "node_modules/unique-filename": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", @@ -1739,6 +2088,14 @@ "aikido-pnpx": "bin/aikido-pnpx.js", "aikido-yarn": "bin/aikido-yarn.js", "safe-chain": "bin/safe-chain.js" + }, + "devDependencies": { + "@types/make-fetch-happen": "^10.0.4", + "@types/node": "^24.9.2", + "@types/node-forge": "^1.3.14", + "@types/npm-registry-fetch": "^8.0.9", + "@types/semver": "^7.7.1", + "typescript": "^5.9.3" } }, "packages/safe-chain-bun": { diff --git a/packages/safe-chain-bun/src/index.js b/packages/safe-chain-bun/src/index.js index 660e0bd..6e933c3 100644 --- a/packages/safe-chain-bun/src/index.js +++ b/packages/safe-chain-bun/src/index.js @@ -29,7 +29,7 @@ export const scanner = { }); } } - } catch (error) { + } catch (/** @type any */ error) { console.warn(`Safe-Chain security scan failed: ${error.message}`); } diff --git a/packages/safe-chain/bin/aikido-bun.js b/packages/safe-chain/bin/aikido-bun.js index 01e3972..2467512 100755 --- a/packages/safe-chain/bin/aikido-bun.js +++ b/packages/safe-chain/bin/aikido-bun.js @@ -7,4 +7,5 @@ const packageManagerName = "bun"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-bunx.js b/packages/safe-chain/bin/aikido-bunx.js index fb378e5..6a84ff8 100755 --- a/packages/safe-chain/bin/aikido-bunx.js +++ b/packages/safe-chain/bin/aikido-bunx.js @@ -7,4 +7,5 @@ const packageManagerName = "bunx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npm.js b/packages/safe-chain/bin/aikido-npm.js index 0e9f302..4a82c34 100755 --- a/packages/safe-chain/bin/aikido-npm.js +++ b/packages/safe-chain/bin/aikido-npm.js @@ -7,4 +7,5 @@ const packageManagerName = "npm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-npx.js b/packages/safe-chain/bin/aikido-npx.js index d3dfdd6..40118d4 100755 --- a/packages/safe-chain/bin/aikido-npx.js +++ b/packages/safe-chain/bin/aikido-npx.js @@ -7,4 +7,5 @@ const packageManagerName = "npx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpm.js b/packages/safe-chain/bin/aikido-pnpm.js index 0a06217..e495568 100755 --- a/packages/safe-chain/bin/aikido-pnpm.js +++ b/packages/safe-chain/bin/aikido-pnpm.js @@ -7,4 +7,5 @@ const packageManagerName = "pnpm"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-pnpx.js b/packages/safe-chain/bin/aikido-pnpx.js index cdb6504..75f093d 100755 --- a/packages/safe-chain/bin/aikido-pnpx.js +++ b/packages/safe-chain/bin/aikido-pnpx.js @@ -7,4 +7,5 @@ const packageManagerName = "pnpx"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/bin/aikido-yarn.js b/packages/safe-chain/bin/aikido-yarn.js index fd87606..3ca5b94 100755 --- a/packages/safe-chain/bin/aikido-yarn.js +++ b/packages/safe-chain/bin/aikido-yarn.js @@ -7,4 +7,5 @@ const packageManagerName = "yarn"; initializePackageManager(packageManagerName); var exitCode = await main(process.argv.slice(2)); +// @ts-expect-error scanCommand can return an empty array in main process.exit(exitCode); diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 98ccd52..4e55840 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -4,7 +4,8 @@ "scripts": { "test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'", "test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'", - "lint": "oxlint --deny-warnings" + "lint": "oxlint --deny-warnings", + "typecheck": "tsc --noEmit" }, "bin": { "aikido-npm": "bin/aikido-npm.js", @@ -38,6 +39,14 @@ "ora": "8.2.0", "semver": "7.7.2" }, + "devDependencies": { + "@types/make-fetch-happen": "^10.0.4", + "@types/node": "^24.9.2", + "@types/npm-registry-fetch": "^8.0.9", + "@types/semver": "^7.7.1", + "@types/node-forge": "^1.3.14", + "typescript": "^5.9.3" + }, "main": "src/main.js", "bugs": { "url": "https://github.com/AikidoSec/safe-chain/issues" diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index c9eeea0..7e786d1 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -3,6 +3,16 @@ import fetch from "make-fetch-happen"; const malwareDatabaseUrl = "https://malware-list.aikido.dev/malware_predictions.json"; +/** + * @typedef MalwarePackage + * @property {string} package_name + * @property {string} version + * @property {string} reason + */ + +/** + * @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>} + */ export async function fetchMalwareDatabase() { const response = await fetch(malwareDatabaseUrl); if (!response.ok) { @@ -15,11 +25,14 @@ export async function fetchMalwareDatabase() { malwareDatabase: malwareDatabase, version: response.headers.get("etag") || undefined, }; - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error(`Error parsing malware database: ${error.message}`); } } +/** + * @returns {Promise} + */ export async function fetchMalwareDatabaseVersion() { const response = await fetch(malwareDatabaseUrl, { method: "HEAD", diff --git a/packages/safe-chain/src/api/npmApi.js b/packages/safe-chain/src/api/npmApi.js index deb917d..c8e2218 100644 --- a/packages/safe-chain/src/api/npmApi.js +++ b/packages/safe-chain/src/api/npmApi.js @@ -1,6 +1,11 @@ import * as semver from "semver"; import * as npmFetch from "npm-registry-fetch"; +/** + * @param {string} packageName + * @param {string | null} [versionRange] + * @returns {Promise} + */ export async function resolvePackageVersion(packageName, versionRange) { if (!versionRange) { versionRange = "latest"; @@ -11,7 +16,10 @@ export async function resolvePackageVersion(packageName, versionRange) { return versionRange; } - const packageInfo = await getPackageInfo(packageName); + const packageInfo = ( + /** @type {{"dist-tags"?: Record} | null} */ + await getPackageInfo(packageName) + ); if (!packageInfo) { // It is possible that no version is found (could be a private package, or a package that doesn't exist) // In this case, we return null to indicate that we couldn't resolve the version @@ -19,7 +27,7 @@ export async function resolvePackageVersion(packageName, versionRange) { } const distTags = packageInfo["dist-tags"]; - if (distTags && distTags[versionRange]) { + if (distTags && isDistTags(distTags)) { // If the version range is a dist-tag, return the version associated with that tag // e.g., "latest", "next", etc. return distTags[versionRange]; @@ -41,6 +49,19 @@ export async function resolvePackageVersion(packageName, versionRange) { return null; } +/** + * + * @param {unknown} distTags + * @returns {distTags is Record} + */ +function isDistTags(distTags) { + return typeof distTags === "object"; +} + +/** + * @param {string} packageName + * @returns {Promise | null>} + */ async function getPackageInfo(packageName) { try { return await npmFetch.json(packageName); diff --git a/packages/safe-chain/src/config/cliArguments.js b/packages/safe-chain/src/config/cliArguments.js index f234bbb..04645d8 100644 --- a/packages/safe-chain/src/config/cliArguments.js +++ b/packages/safe-chain/src/config/cliArguments.js @@ -1,9 +1,16 @@ +/** + * @type {{loggingLevel: string | undefined}} + */ const state = { loggingLevel: undefined, }; const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-"; +/** + * @param {string[]} args + * @returns {string[]} + */ export function initializeCliArguments(args) { // Reset state on each call state.loggingLevel = undefined; @@ -24,6 +31,11 @@ export function initializeCliArguments(args) { return remainingArgs; } +/** + * @param {string[]} args + * @param {string} prefix + * @returns {string | undefined} + */ function getLastArgEqualsValue(args, prefix) { for (var i = args.length - 1; i >= 0; i--) { const arg = args[i]; @@ -35,6 +47,10 @@ function getLastArgEqualsValue(args, prefix) { return undefined; } +/** + * @param {string[]} args + * @returns {void} + */ function setLoggingLevel(args) { const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging="; diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 2feb307..83bc186 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -3,13 +3,46 @@ import path from "path"; import os from "os"; import { ui } from "../environment/userInteraction.js"; +/** + * @returns {number} + */ export function getScanTimeout() { + if (process.env.AIKIDO_SCAN_TIMEOUT_MS) { + const timeout = parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS); + if (!isNaN(timeout) && timeout >= 0) { + return timeout; + } + } + const config = readConfigFile(); + + if (hasScanTimeout(config) && config.scanTimeout >= 0) { + return config.scanTimeout; + } + + return 10000; // Default to 10 seconds +} + +/** + * @param {unknown} config + * + * @returns {config is {scanTimeout: number}} + */ +function hasScanTimeout(config) { return ( - parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds + typeof config === "object" && + config !== null && + "scanTimeout" in config && + typeof config.scanTimeout === "number" ); } +/** + * @param {import("../api/aikido.js").MalwarePackage[]} data + * @param {string | number} version + * + * @returns {void} + */ export function writeDatabaseToLocalCache(data, version) { try { const databasePath = getDatabasePath(); @@ -24,6 +57,9 @@ export function writeDatabaseToLocalCache(data, version) { } } +/** + * @returns {{malwareDatabase: import("../api/aikido.js").MalwarePackage[] | null, version: string | null}} + */ export function readDatabaseFromLocalCache() { try { const databasePath = getDatabasePath(); @@ -55,6 +91,9 @@ export function readDatabaseFromLocalCache() { } } +/** + * @returns {unknown} + */ function readConfigFile() { const configFilePath = getConfigFilePath(); @@ -66,20 +105,30 @@ function readConfigFile() { return JSON.parse(data); } +/** + * @returns {string} + */ function getDatabasePath() { const aikidoDir = getAikidoDirectory(); return path.join(aikidoDir, "malwareDatabase.json"); } + function getDatabaseVersionPath() { const aikidoDir = getAikidoDirectory(); return path.join(aikidoDir, "version.txt"); } +/** + * @returns {string} + */ function getConfigFilePath() { return path.join(getAikidoDirectory(), "config.json"); } +/** + * @returns {string} + */ function getAikidoDirectory() { const homeDir = os.homedir(); const aikidoDir = path.join(homeDir, ".aikido"); diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index e1a4f93..43f5312 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -14,12 +14,22 @@ function emptyLine() { writeInformation(""); } +/** + * @param {string} message + * @param {...any} optionalParams + * @returns {void} + */ function writeInformation(message, ...optionalParams) { if (isSilentMode()) return; console.log(message, ...optionalParams); } +/** + * @param {string} message + * @param {...any} optionalParams + * @returns {void} + */ function writeWarning(message, ...optionalParams) { if (isSilentMode()) return; @@ -29,6 +39,11 @@ function writeWarning(message, ...optionalParams) { console.warn(message, ...optionalParams); } +/** + * @param {string} message + * @param {...any} optionalParams + * @returns {void} + */ function writeError(message, ...optionalParams) { if (!isCi()) { message = chalk.red(message); @@ -44,6 +59,19 @@ function writeExitWithoutInstallingMaliciousPackages() { console.error(message); } +/** + * @typedef Spinner + * @property {(message: string) => void} succeed + * @property {(message: string) => void} fail + * @property {() => void} stop + * @property {(message: string) => void} setText + */ + +/** + * @param {string} message + * + * @returns {Spinner} + */ function startProcess(message) { if (isSilentMode()) { return { diff --git a/packages/safe-chain/src/main.js b/packages/safe-chain/src/main.js index c3af410..fca1218 100644 --- a/packages/safe-chain/src/main.js +++ b/packages/safe-chain/src/main.js @@ -7,6 +7,10 @@ import { initializeCliArguments } from "./config/cliArguments.js"; import { createSafeChainProxy } from "./registryProxy/registryProxy.js"; import chalk from "chalk"; +/** + * @param {string[]} args + * @returns {Promise} + */ export async function main(args) { const proxy = createSafeChainProxy(); await proxy.startServer(); @@ -14,6 +18,7 @@ export async function main(args) { // Global error handlers to log unhandled errors process.on("uncaughtException", (error) => { ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`); + // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${error.stack}`); process.exit(1); }); @@ -21,6 +26,7 @@ export async function main(args) { process.on("unhandledRejection", (reason) => { ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`); if (reason instanceof Error) { + // @ts-expect-error writeVerbose will be added in a future PR ui.writeVerbose(`Stack trace: ${reason.stack}`); } process.exit(1); @@ -56,7 +62,7 @@ export async function main(args) { // Returning the exit code back to the caller allows the promise // to be awaited in the bin files and return the correct exit code return packageManagerResult.status; - } catch (error) { + } catch (/** @type any */ error) { ui.writeError("Failed to check for malicious packages:", error.message); // Returning the exit code back to the caller allows the promise diff --git a/packages/safe-chain/src/packagemanager/_shared/matchesCommand.js b/packages/safe-chain/src/packagemanager/_shared/matchesCommand.js index d72caca..f939352 100644 --- a/packages/safe-chain/src/packagemanager/_shared/matchesCommand.js +++ b/packages/safe-chain/src/packagemanager/_shared/matchesCommand.js @@ -1,3 +1,8 @@ +/** + * @param {string[]} args + * @param {...string} commandArgs + * @returns {boolean} + */ export function matchesCommand(args, ...commandArgs) { if (args.length < commandArgs.length) { return false; diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js index 14faa5f..88c84f5 100644 --- a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -2,6 +2,9 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createBunPackageManager() { return { runCommand: (args) => runBunCommand("bun", args), @@ -9,10 +12,13 @@ export function createBunPackageManager() { // For bun, we use the proxy-only approach to block package downloads, // so we don't need to analyze commands. isSupportedCommand: () => false, - getDependencyUpdatesForCommand: () => [], + getDependencyUpdatesForCommand: async () => [], }; } +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createBunxPackageManager() { return { runCommand: (args) => runBunCommand("bunx", args), @@ -20,18 +26,24 @@ export function createBunxPackageManager() { // For bunx, we use the proxy-only approach to block package downloads, // so we don't need to analyze commands. isSupportedCommand: () => false, - getDependencyUpdatesForCommand: () => [], + getDependencyUpdatesForCommand: async () => [], }; } +/** + * @param {string} command + * @param {string[]} args + * @returns {Promise<{status: number}>} + */ async function runBunCommand(command, args) { try { const result = await safeSpawn(command, args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index 2f019a1..aaf8aae 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -10,10 +10,25 @@ import { } from "./pnpm/createPackageManager.js"; import { createYarnPackageManager } from "./yarn/createPackageManager.js"; +/** + * @type {{packageManagerName: PackageManager | null}} + */ const state = { packageManagerName: null, }; +/** + * @typedef PackageManager + * @property {(args: string[]) => Promise<{ status: number }>} runCommand + * @property {(args: string[]) => boolean} isSupportedCommand + * @property {(args: string[]) => Promise<{name: string, version: string, type: string}[]>} getDependencyUpdatesForCommand + */ + +/** + * @param {string} packageManagerName + * + * @return {PackageManager} + */ export function initializePackageManager(packageManagerName) { if (packageManagerName === "npm") { state.packageManagerName = createNpmPackageManager(); diff --git a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js index 731f406..465bf60 100644 --- a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js @@ -8,7 +8,15 @@ import { npmExecCommand, } from "./utils/npmCommands.js"; +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createNpmPackageManager() { + /** + * @param {string[]} args + * + * @returns {boolean} + */ function isSupportedCommand(args) { const scanner = findDependencyScannerForCommand( commandScannerMapping, @@ -17,6 +25,11 @@ export function createNpmPackageManager() { return scanner.shouldScan(args); } + /** + * @param {string[]} args + * + * @returns {Promise<{name: string, version: string, type: string}[]>} + */ function getDependencyUpdatesForCommand(args) { const scanner = findDependencyScannerForCommand( commandScannerMapping, @@ -32,12 +45,22 @@ export function createNpmPackageManager() { }; } +/** + * @type {Record} + */ const commandScannerMapping = { [npmInstallCommand]: commandArgumentScanner(), [npmUpdateCommand]: commandArgumentScanner(), [npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run }; +/** + * + * @param {Record} scanners + * @param {string[]} args + * + * @returns {import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} + */ function findDependencyScannerForCommand(scanners, args) { const command = getNpmCommandForArgs(args); if (!command) { diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js index ae05f6d..51746c4 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js @@ -2,6 +2,29 @@ import { resolvePackageVersion } from "../../../api/npmApi.js"; import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js"; import { hasDryRunArg } from "../utils/npmCommands.js"; +/** + * @typedef {Object} ScanResult + * @property {string} name + * @property {string} version + * @property {string} type + */ + +/** + * @typedef {Object} ScannerOptions + * @property {boolean} [ignoreDryRun] + */ + +/** + * @typedef CommandArgumentScanner + * @property {(args: string[]) => Promise} scan + * @property {(args: string[]) => boolean} shouldScan + */ + +/** + * @param {ScannerOptions} [opts] + * + * @returns {CommandArgumentScanner} + */ export function commandArgumentScanner(opts) { const ignoreDryRun = opts?.ignoreDryRun ?? false; @@ -10,14 +33,28 @@ export function commandArgumentScanner(opts) { shouldScan: (args) => shouldScanDependencies(args, ignoreDryRun), }; } + +/** + * @param {string[]} args + * @returns {Promise} + */ function scanDependencies(args) { return checkChangesFromArgs(args); } +/** + * @param {string[]} args + * @param {boolean} ignoreDryRun + * @returns {boolean} + */ function shouldScanDependencies(args, ignoreDryRun) { return ignoreDryRun || !hasDryRunArg(args); } +/** + * @param {string[]} args + * @returns {Promise} + */ export async function checkChangesFromArgs(args) { const changes = []; const packageUpdates = parsePackagesFromInstallArgs(args); diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js index a7b2ffd..449fed4 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js @@ -1,6 +1,9 @@ +/** + * @returns {import("./commandArgumentScanner.js").CommandArgumentScanner} + */ export function nullScanner() { return { - scan: () => [], + scan: async () => [], shouldScan: () => false, }; } diff --git a/packages/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js b/packages/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js index e731240..b7277e7 100644 --- a/packages/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js +++ b/packages/safe-chain/src/packagemanager/npm/parsing/parsePackagesFromInstallArgs.js @@ -1,5 +1,22 @@ +/** + * @typedef {Object} PackageDetail + * @property {string} name + * @property {string} version + */ + +/** + * @typedef {Object} NpmOption + * @property {string} name + * @property {number} numberOfParameters + */ + +/** + * @param {string[]} args + * @returns {PackageDetail[]} + */ export function parsePackagesFromInstallArgs(args) { - const changes = []; + /** @type {{name: string, version: string | null}[]} */ + const changes = []; let defaultTag = "latest"; // Skip first argument (install command) @@ -32,9 +49,13 @@ export function parsePackagesFromInstallArgs(args) { } } - return changes; + return /** @type {PackageDetail[]} */ (changes); } +/** + * @param {string} arg + * @returns {NpmOption | undefined} + */ function getNpmOption(arg) { if (isNpmOptionWithParameter(arg)) { return { @@ -54,6 +75,10 @@ function getNpmOption(arg) { return undefined; } +/** + * @param {string} arg + * @returns {boolean} + */ function isNpmOptionWithParameter(arg) { const optionsWithParameters = [ "--access", @@ -81,6 +106,10 @@ function isNpmOptionWithParameter(arg) { return optionsWithParameters.includes(arg); } +/** + * @param {string} arg + * @returns {{name: string, version: string | null}} + */ function parsePackagename(arg) { arg = removeAlias(arg); const lastAtIndex = arg.lastIndexOf("@"); @@ -102,6 +131,10 @@ function parsePackagename(arg) { }; } +/** + * @param {string} arg + * @returns {string} + */ function removeAlias(arg) { const aliasIndex = arg.indexOf("@npm:"); if (aliasIndex !== -1) { diff --git a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js index 26a4a9d..c068240 100644 --- a/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js +++ b/packages/safe-chain/src/packagemanager/npm/runNpmCommand.js @@ -2,14 +2,20 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +/** + * @param {string[]} args + * + * @returns {Promise<{status: number}>} + */ export async function runNpm(args) { try { const result = await safeSpawn("npm", args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { @@ -19,6 +25,10 @@ export async function runNpm(args) { } } +/** + * @param {string[]} args + * @returns {Promise<{status: number, output?: string}>} + */ export async function dryRunNpmCommandAndOutput(args) { try { const result = await safeSpawn( @@ -26,6 +36,7 @@ export async function dryRunNpmCommandAndOutput(args) { [...args, "--ignore-scripts", "--dry-run"], { stdio: "pipe", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), } ); @@ -33,7 +44,7 @@ export async function dryRunNpmCommandAndOutput(args) { status: result.status, output: result.status === 0 ? result.stdout : result.stderr, }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { const output = error.stdout?.toString() ?? diff --git a/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js b/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js index 204ffa7..8e76ad1 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/abbrevs-generated.js @@ -1,5 +1,6 @@ // This was ran with the abbrev package to generate the abbrevs object below // console.log(abbrev(commands.concat(Object.keys(aliases)))); +/** @type {Record} */ export const abbrevs = { ac: "access", acc: "access", diff --git a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js index 6e67520..3bcdd0d 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/cmd-list.js @@ -73,6 +73,7 @@ const commands = [ ]; // These must resolve to an entry in commands +/** @type {Record} */ const aliases = { // aliases author: "owner", @@ -138,6 +139,10 @@ const aliases = { "add-user": "adduser", }; +/** + * @param {string} c + * @returns {string | undefined} + */ export function deref(c) { if (!c) { return; diff --git a/packages/safe-chain/src/packagemanager/npm/utils/npmCommands.js b/packages/safe-chain/src/packagemanager/npm/utils/npmCommands.js index 3096144..b645369 100644 --- a/packages/safe-chain/src/packagemanager/npm/utils/npmCommands.js +++ b/packages/safe-chain/src/packagemanager/npm/utils/npmCommands.js @@ -1,5 +1,9 @@ import { deref } from "./cmd-list.js"; +/** + * @param {string[]} args + * @returns {string | null} + */ export function getNpmCommandForArgs(args) { if (args.length === 0) { return null; @@ -13,6 +17,10 @@ export function getNpmCommandForArgs(args) { return argCommand; } +/** + * @param {string[]} args + * @returns {boolean} + */ export function hasDryRunArg(args) { return args.some((arg) => arg === "--dry-run"); } diff --git a/packages/safe-chain/src/packagemanager/npx/createPackageManager.js b/packages/safe-chain/src/packagemanager/npx/createPackageManager.js index a3319fa..96d495b 100644 --- a/packages/safe-chain/src/packagemanager/npx/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/npx/createPackageManager.js @@ -1,6 +1,9 @@ import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js"; import { runNpx } from "./runNpxCommand.js"; +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createNpxPackageManager() { const scanner = commandArgumentScanner(); diff --git a/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js index 16328cb..7f4da9f 100644 --- a/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js @@ -1,16 +1,28 @@ import { resolvePackageVersion } from "../../../api/npmApi.js"; import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js"; +/** + * @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} + */ export function commandArgumentScanner() { return { scan: (args) => scanDependencies(args), - shouldScan: () => true, // all npx commands need to be scanned, npx doesn't have dry-run + shouldScan: (args) => true, // all npx commands need to be scanned, npx doesn't have dry-run }; } + +/** + * @param {string[]} args + * @returns {Promise} + */ function scanDependencies(args) { return checkChangesFromArgs(args); } +/** + * @param {string[]} args + * @returns {Promise} + */ export async function checkChangesFromArgs(args) { const changes = []; const packageUpdates = parsePackagesFromArguments(args); diff --git a/packages/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js b/packages/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js index efc8d81..25fb249 100644 --- a/packages/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js +++ b/packages/safe-chain/src/packagemanager/npx/parsing/parsePackagesFromArguments.js @@ -1,3 +1,8 @@ +/** + * @param {string[]} args + * + * @returns {{name: string, version: string}[]} + */ export function parsePackagesFromArguments(args) { let defaultTag = "latest"; @@ -21,6 +26,10 @@ export function parsePackagesFromArguments(args) { return []; } +/** + * @param {string} arg + * @returns {{name: string, numberOfParameters: number} | undefined} + */ function getOption(arg) { if (isOptionWithParameter(arg)) { return { @@ -41,6 +50,10 @@ function getOption(arg) { return undefined; } +/** + * @param {string} arg + * @returns {boolean} + */ function isOptionWithParameter(arg) { const optionsWithParameters = [ "--access", @@ -68,6 +81,11 @@ function isOptionWithParameter(arg) { return optionsWithParameters.includes(arg); } +/** + * @param {string} arg + * @param {string} defaultTag + * @returns {{name: string, version: string}} + */ function parsePackagename(arg, defaultTag) { // format can be --package=name@version // in that case, we need to remove the --package= part @@ -97,6 +115,10 @@ function parsePackagename(arg, defaultTag) { }; } +/** + * @param {string} arg + * @returns {string} + */ function removeAlias(arg) { // removes the alias. // Eg.: server@npm:http-server@latest becomes http-server@latest diff --git a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js index b8896b7..61adcaa 100644 --- a/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js +++ b/packages/safe-chain/src/packagemanager/npx/runNpxCommand.js @@ -2,14 +2,20 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +/** + * @param {string[]} args + * + * @returns {Promise<{status: number}>} + */ export async function runNpx(args) { try { const result = await safeSpawn("npx", args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { diff --git a/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js b/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js index 15cb628..193470b 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js @@ -4,6 +4,9 @@ import { runPnpmCommand } from "./runPnpmCommand.js"; const scanner = commandArgumentScanner(); +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createPnpmPackageManager() { return { runCommand: (args) => runPnpmCommand(args, "pnpm"), @@ -23,6 +26,9 @@ export function createPnpmPackageManager() { }; } +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createPnpxPackageManager() { return { runCommand: (args) => runPnpmCommand(args, "pnpx"), @@ -32,6 +38,11 @@ export function createPnpxPackageManager() { }; } +/** + * @param {string[]} args + * @param {boolean} isPnpx + * @returns {Promise} + */ function getDependencyUpdatesForCommand(args, isPnpx) { if (isPnpx) { return scanner.scan(args); diff --git a/packages/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js index c184b38..e46d2db 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/pnpm/dependencyScanner/commandArgumentScanner.js @@ -1,6 +1,9 @@ import { resolvePackageVersion } from "../../../api/npmApi.js"; import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js"; +/** + * @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} + */ export function commandArgumentScanner() { return { scan: (args) => scanDependencies(args), @@ -8,6 +11,10 @@ export function commandArgumentScanner() { }; } +/** + * @param {string[]} args + * @returns {Promise} + */ async function scanDependencies(args) { const changes = []; const packageUpdates = parsePackagesFromArguments(args); diff --git a/packages/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js b/packages/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js index d0383c2..b8a6f39 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js +++ b/packages/safe-chain/src/packagemanager/pnpm/parsing/parsePackagesFromArguments.js @@ -1,3 +1,7 @@ +/** + * @param {string[]} args + * @returns {{name: string, version: string}[]} + */ export function parsePackagesFromArguments(args) { const changes = []; let defaultTag = "latest"; @@ -22,6 +26,10 @@ export function parsePackagesFromArguments(args) { return changes; } +/** + * @param {string} arg + * @returns {{name: string, numberOfParameters: number} | undefined} + */ function getOption(arg) { if (isOptionWithParameter(arg)) { return { @@ -42,12 +50,21 @@ function getOption(arg) { return undefined; } +/** + * @param {string} arg + * @returns {boolean} + */ function isOptionWithParameter(arg) { const optionsWithParameters = ["--C", "--dir"]; return optionsWithParameters.includes(arg); } +/** + * @param {string} arg + * @param {string} defaultTag + * @returns {{name: string, version: string}} + */ function parsePackagename(arg, defaultTag) { // format can be --package=name@version // in that case, we need to remove the --package= part @@ -77,6 +94,10 @@ function parsePackagename(arg, defaultTag) { }; } +/** + * @param {string} arg + * @returns {string} + */ function removeAlias(arg) { // removes the alias. // Eg.: server@npm:http-server@latest becomes http-server@latest diff --git a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js index 794d6e3..db4ffa9 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js +++ b/packages/safe-chain/src/packagemanager/pnpm/runPnpmCommand.js @@ -2,17 +2,24 @@ import { ui } from "../../environment/userInteraction.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; +/** + * @param {string[]} args + * @param {string} [toolName] + * @returns {Promise<{status: number}>} + */ export async function runPnpmCommand(args, toolName = "pnpm") { try { let result; if (toolName === "pnpm") { result = await safeSpawn("pnpm", args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else if (toolName === "pnpx") { result = await safeSpawn("pnpx", args, { stdio: "inherit", + // @ts-expect-error values of process.env can be string | undefined env: mergeSafeChainProxyEnvironmentVariables(process.env), }); } else { @@ -20,7 +27,7 @@ export async function runPnpmCommand(args, toolName = "pnpm") { } return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { diff --git a/packages/safe-chain/src/packagemanager/yarn/createPackageManager.js b/packages/safe-chain/src/packagemanager/yarn/createPackageManager.js index f49c763..f8a0c84 100644 --- a/packages/safe-chain/src/packagemanager/yarn/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/yarn/createPackageManager.js @@ -3,6 +3,9 @@ import { runYarnCommand } from "./runYarnCommand.js"; const scanner = commandArgumentScanner(); +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createYarnPackageManager() { return { runCommand: runYarnCommand, @@ -18,6 +21,11 @@ export function createYarnPackageManager() { }; } +/** + * @param {string[]} args + * @param {...string} commandArgs + * @returns {boolean} + */ function matchesCommand(args, ...commandArgs) { if (args.length < commandArgs.length) { return false; diff --git a/packages/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js index f5bdd9f..5141d54 100644 --- a/packages/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/yarn/dependencyScanner/commandArgumentScanner.js @@ -1,6 +1,9 @@ import { resolvePackageVersion } from "../../../api/npmApi.js"; import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js"; +/** + * @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner} + */ export function commandArgumentScanner() { return { scan: (args) => scanDependencies(args), @@ -8,6 +11,10 @@ export function commandArgumentScanner() { }; } +/** + * @param {string[]} args + * @returns {Promise} + */ async function scanDependencies(args) { const changes = []; const packageUpdates = parsePackagesFromArguments(args); diff --git a/packages/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js b/packages/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js index 7b0255e..8f97de5 100644 --- a/packages/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js +++ b/packages/safe-chain/src/packagemanager/yarn/parsing/parsePackagesFromArguments.js @@ -1,3 +1,7 @@ +/** + * @param {string[]} args + * @returns {{name: string, version: string}[]} + */ export function parsePackagesFromArguments(args) { const changes = []; let defaultTag = "latest"; @@ -22,6 +26,11 @@ export function parsePackagesFromArguments(args) { return changes; } +/** + * @param {string} arg + * + * @returns {{name: string, numberOfParameters: number} | undefined} + */ function getOption(arg) { if (isOptionWithParameter(arg)) { return { @@ -42,6 +51,11 @@ function getOption(arg) { return undefined; } +/** + * @param {string} arg + * + * @returns {boolean} + */ function isOptionWithParameter(arg) { const optionsWithParameters = [ "--use-yarnrc", @@ -64,6 +78,12 @@ function isOptionWithParameter(arg) { return optionsWithParameters.includes(arg); } +/** + * @param {string} arg + * @param {string} defaultTag + * + * @returns {{name: string, version: string}} + */ function parsePackagename(arg, defaultTag) { // format can be --package=name@version // in that case, we need to remove the --package= part @@ -93,6 +113,10 @@ function parsePackagename(arg, defaultTag) { }; } +/** + * @param {string} arg + * @returns {string} + */ function removeAlias(arg) { // removes the alias. // Eg.: server@npm:http-server@latest becomes http-server@latest diff --git a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js index 65c27a0..1ba0f5c 100644 --- a/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js +++ b/packages/safe-chain/src/packagemanager/yarn/runYarnCommand.js @@ -2,8 +2,14 @@ import { ui } from "../../environment/userInteraction.js"; import { safeSpawn } from "../../utils/safeSpawn.js"; import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js"; +/** + * @param {string[]} args + * + * @returns {Promise<{status: number}>} + */ export async function runYarnCommand(args) { try { + // @ts-expect-error values of process.env can be string | undefined const env = mergeSafeChainProxyEnvironmentVariables(process.env); await fixYarnProxyEnvironmentVariables(env); @@ -12,7 +18,7 @@ export async function runYarnCommand(args) { env, }); return { status: result.status }; - } catch (error) { + } catch (/** @type any */ error) { if (error.status) { return { status: error.status }; } else { @@ -22,6 +28,11 @@ export async function runYarnCommand(args) { } } +/** + * @param {Record} env + * + * @returns {Promise} + */ async function fixYarnProxyEnvironmentVariables(env) { // Yarn ignores standard proxy environment variable HTTPS_PROXY // It does respect NODE_EXTRA_CA_CERTS for custom CA certificates though. diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index d5d414c..a2fb7bb 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -12,6 +12,10 @@ export function getCaCertPath() { return path.join(certFolder, "ca-cert.pem"); } +/** + * @param {string} hostname + * @returns {{privateKey: string, certificate: string}} + */ export function generateCertForHost(hostname) { let existingCert = certCache.get(hostname); if (existingCert) { diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index fe8998e..be2b357 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -3,6 +3,11 @@ import { generateCertForHost } from "./certUtils.js"; import { HttpsProxyAgent } from "https-proxy-agent"; import { ui } from "../environment/userInteraction.js"; +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {(target: string) => Promise} isAllowed + */ export function mitmConnect(req, clientSocket, isAllowed) { const { hostname } = new URL(`http://${req.url}`); @@ -16,6 +21,7 @@ export function mitmConnect(req, clientSocket, isAllowed) { server.on("error", (err) => { ui.writeError(`Safe-chain: HTTPS server error: ${err.message}`); + // @ts-expect-error Property 'headersSent' does not exist on type 'Socket' if (!clientSocket.headersSent) { clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); } else if (clientSocket.writable) { @@ -30,10 +36,22 @@ export function mitmConnect(req, clientSocket, isAllowed) { server.emit("connection", clientSocket); } +/** + * @param {string} hostname + * @param {(target: string) => Promise} isAllowed + * @returns {import("https").Server} + */ function createHttpsServer(hostname, isAllowed) { const cert = generateCertForHost(hostname); + /** + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} res + * + * @returns {Promise} + */ async function handleRequest(req, res) { + // @ts-expect-error req.url might be undefined const pathAndQuery = getRequestPathAndQuery(req.url); const targetUrl = `https://${hostname}${pathAndQuery}`; @@ -58,6 +76,10 @@ function createHttpsServer(hostname, isAllowed) { return server; } +/** + * @param {string} url + * @returns {*|string} + */ function getRequestPathAndQuery(url) { if (url.startsWith("http://") || url.startsWith("https://")) { const parsedUrl = new URL(url); @@ -66,6 +88,11 @@ function getRequestPathAndQuery(url) { return url; } +/** + * @param {import("http").IncomingMessage} req + * @param {string} hostname + * @param {import("http").ServerResponse} res + */ function forwardRequest(req, hostname, res) { const proxyReq = createProxyRequest(hostname, req, res); @@ -88,7 +115,15 @@ function forwardRequest(req, hostname, res) { }); } +/** + * @param {string} hostname + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} res + * + * @returns {import("http").ClientRequest} + */ function createProxyRequest(hostname, req, res) { + /** @type {import("http").RequestOptions} */ const options = { hostname: hostname, port: 443, @@ -97,7 +132,9 @@ function createProxyRequest(hostname, req, res) { headers: { ...req.headers }, }; - delete options.headers.host; + if (options.headers && "host" in options.headers) { + delete options.headers["host"]; + } const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; if (httpsProxy) { @@ -115,6 +152,7 @@ function createProxyRequest(hostname, req, res) { } }); + // @ts-expect-error statusCode might be undefined res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }); diff --git a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js index 7368b35..bffb182 100644 --- a/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js +++ b/packages/safe-chain/src/registryProxy/parsePackageFromUrl.js @@ -1,5 +1,9 @@ export const knownRegistries = ["registry.npmjs.org", "registry.yarnpkg.com"]; +/** + * @param {string} url + * @returns {{packageName: string | undefined, version: string | undefined}} + */ export function parsePackageFromUrl(url) { let packageName, version, registry; diff --git a/packages/safe-chain/src/registryProxy/plainHttpProxy.js b/packages/safe-chain/src/registryProxy/plainHttpProxy.js index ac3dc69..16b305a 100644 --- a/packages/safe-chain/src/registryProxy/plainHttpProxy.js +++ b/packages/safe-chain/src/registryProxy/plainHttpProxy.js @@ -1,7 +1,14 @@ import * as http from "http"; import * as https from "https"; +/** + * @param {import("http").IncomingMessage} req + * @param {import("http").ServerResponse} res + * + * @returns {void} + */ export function handleHttpProxyRequest(req, res) { + // @ts-expect-error req.url might be undefined const url = new URL(req.url); // The protocol for the plainHttpProxy should usually only be http: @@ -20,9 +27,11 @@ export function handleHttpProxyRequest(req, res) { const proxyRequest = protocol .request( + // @ts-expect-error req.url might be undefined req.url, { method: req.method, headers: req.headers }, (proxyRes) => { + // @ts-expect-error statusCode might be undefined res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); diff --git a/packages/safe-chain/src/registryProxy/registryProxy.js b/packages/safe-chain/src/registryProxy/registryProxy.js index 887fd47..bd98ff5 100644 --- a/packages/safe-chain/src/registryProxy/registryProxy.js +++ b/packages/safe-chain/src/registryProxy/registryProxy.js @@ -9,6 +9,9 @@ import { ui } from "../environment/userInteraction.js"; import chalk from "chalk"; const SERVER_STOP_TIMEOUT_MS = 1000; +/** + * @type {{port: number | null, blockedRequests: {packageName: string, version: string, url: string}[]}} + */ const state = { port: null, blockedRequests: [], @@ -24,6 +27,9 @@ export function createSafeChainProxy() { }; } +/** + * @returns {Record} + */ function getSafeChainProxyEnvironmentVariables() { if (!state.port) { return {}; @@ -36,6 +42,11 @@ function getSafeChainProxyEnvironmentVariables() { }; } +/** + * @param {Record} env + * + * @returns {Record} + */ export function mergeSafeChainProxyEnvironmentVariables(env) { const proxyEnv = getSafeChainProxyEnvironmentVariables(); @@ -67,6 +78,11 @@ function createProxyServer() { return server; } +/** + * @param {import("http").Server} server + * + * @returns {Promise} + */ function startServer(server) { return new Promise((resolve, reject) => { // Passing port 0 makes the OS assign an available port @@ -86,6 +102,11 @@ function startServer(server) { }); } +/** + * @param {import("http").Server} server + * + * @returns {Promise} + */ function stopServer(server) { return new Promise((resolve) => { try { @@ -99,10 +120,18 @@ function stopServer(server) { }); } +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {Buffer} head + * + * @returns {void} + */ function handleConnect(req, clientSocket, head) { // CONNECT method is used for HTTPS requests // It establishes a tunnel to the server identified by the request URL + // @ts-expect-error req.url might be undefined if (knownRegistries.some((reg) => req.url.includes(reg))) { // For npm and yarn registries, we want to intercept and inspect the traffic // so we can block packages with malware @@ -113,6 +142,10 @@ function handleConnect(req, clientSocket, head) { } } +/** + * @param {string} url + * @returns {Promise} + */ async function isAllowedUrl(url) { const { packageName, version } = parsePackageFromUrl(url); diff --git a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js index 5c764f5..452ef15 100644 --- a/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/tunnelRequestHandler.js @@ -1,6 +1,13 @@ import * as net from "net"; import { ui } from "../environment/userInteraction.js"; +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {Buffer} head + * + * @returns {void} + */ export function tunnelRequest(req, clientSocket, head) { const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; @@ -21,9 +28,17 @@ export function tunnelRequest(req, clientSocket, head) { } } +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {Buffer} head + * + * @returns {void} + */ function tunnelRequestToDestination(req, clientSocket, head) { const { port, hostname } = new URL(`http://${req.url}`); + // @ts-expect-error port from URL is a string but net.connect accepts number const serverSocket = net.connect(port || 443, hostname, () => { clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); serverSocket.write(head); @@ -49,11 +64,18 @@ function tunnelRequestToDestination(req, clientSocket, head) { }); } +/** + * @param {import("http").IncomingMessage} req + * @param {import("net").Socket} clientSocket + * @param {Buffer} head + * @param {string} proxyUrl + */ function tunnelRequestViaProxy(req, clientSocket, head, proxyUrl) { const { port, hostname } = new URL(`http://${req.url}`); const proxy = new URL(proxyUrl); // Connect to proxy server + // @ts-expect-error net.connect wants port as number but proxy.port is string const proxySocket = net.connect({ host: proxy.hostname, port: proxy.port, diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 215bfa0..8eea526 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -3,6 +3,25 @@ import { openMalwareDatabase, } from "../malwareDatabase.js"; +/** + * @typedef PackageChange + * @property {string} name + * @property {string} version + * @property {string} type + */ + +/** + * @typedef AuditResult + * @property {PackageChange[]} allowedChanges + * @property {(PackageChange & {reason: string})[]} disallowedChanges + * @property {boolean} isAllowed + */ + +/** + * @param {PackageChange[]} changes + * + * @returns {Promise} + */ export async function auditChanges(changes) { const allowedChanges = []; const disallowedChanges = []; @@ -34,6 +53,10 @@ export async function auditChanges(changes) { return auditResults; } +/** + * @param {{name: string, version: string, type: string}[]} changes + * @returns {Promise<{name: string, version: string, status: string}[]>} + */ async function getPackagesWithMalware(changes) { if (changes.length === 0) { return []; diff --git a/packages/safe-chain/src/scanning/index.js b/packages/safe-chain/src/scanning/index.js index 969c994..d8e817e 100644 --- a/packages/safe-chain/src/scanning/index.js +++ b/packages/safe-chain/src/scanning/index.js @@ -5,6 +5,11 @@ import chalk from "chalk"; import { getPackageManager } from "../packagemanager/currentPackageManager.js"; import { ui } from "../environment/userInteraction.js"; +/** + * @param {string[]} args + * + * @returns {boolean} + */ export function shouldScanCommand(args) { if (!args || args.length === 0) { return false; @@ -13,6 +18,11 @@ export function shouldScanCommand(args) { return getPackageManager().isSupportedCommand(args); } +/** + * @param {string[]} args + * + * @returns {Promise} + */ export async function scanCommand(args) { if (!shouldScanCommand(args)) { return []; @@ -23,6 +33,7 @@ export async function scanCommand(args) { const spinner = ui.startProcess( "Safe-chain: Scanning for malicious packages..." ); + /** @type {import("./audit/index.js").AuditResult | undefined} */ let audit; await Promise.race([ @@ -44,7 +55,7 @@ export async function scanCommand(args) { } audit = await auditChanges(changes); - } catch (error) { + } catch (/** @type any */ error) { spinner.fail(`Safe-chain: Error while scanning.`); throw error; } @@ -69,6 +80,12 @@ export async function scanCommand(args) { } } +/** + * @param {import("./audit/index.js").PackageChange[]} changes + * @param spinner {import("../environment/userInteraction.js").Spinner} + * + * @return {void} + */ function printMaliciousChanges(changes, spinner) { spinner.fail("Safe-chain: " + chalk.bold("Malicious changes detected:")); diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 1cb781b..481ff7d 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -8,6 +8,13 @@ import { } from "../config/configFile.js"; import { ui } from "../environment/userInteraction.js"; +/** + * @typedef MalwareDatabase + * @property {function(string, string): string} getPackageStatus + * @property {function(string, string): boolean} isMalware + */ + +/** @type {MalwareDatabase | null} */ let cachedMalwareDatabase = null; export async function openMalwareDatabase() { @@ -17,6 +24,11 @@ export async function openMalwareDatabase() { const malwareDatabase = await getMalwareDatabase(); + /** + * @param {string} name + * @param {string} version + * @returns {string} + */ function getPackageStatus(name, version) { const packageData = malwareDatabase.find( (pkg) => @@ -31,7 +43,7 @@ export async function openMalwareDatabase() { return packageData.reason; } - // This implicitely caches the malware database + // This implicitly caches the malware database // that's closed over by the getPackageStatus function cachedMalwareDatabase = { getPackageStatus, @@ -43,6 +55,9 @@ export async function openMalwareDatabase() { return cachedMalwareDatabase; } +/** + * @returns {Promise} + */ async function getMalwareDatabase() { const { malwareDatabase: cachedDatabase, version: cachedVersion } = readDatabaseFromLocalCache(); @@ -56,10 +71,11 @@ async function getMalwareDatabase() { } const { malwareDatabase, version } = await fetchMalwareDatabase(); + // @ts-expect-error version can be undefined writeDatabaseToLocalCache(malwareDatabase, version); return malwareDatabase; - } catch (error) { + } catch (/** @type any */ error) { if (cachedDatabase) { ui.writeWarning( "Failed to fetch the latest malware database. Using cached version." @@ -70,6 +86,11 @@ async function getMalwareDatabase() { } } +/** + * @param {string} status + * + * @returns {boolean} + */ function isMalwareStatus(status) { let malwareStatus = status.toUpperCase(); return malwareStatus === MALWARE_STATUS_MALWARE; diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 2345022..697f2cb 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,6 +3,15 @@ import * as os from "os"; import fs from "fs"; import path from "path"; +/** + * @typedef AikidoTool + * @property {string} tool + * @property {string} aikidoCommand + */ + +/** + * @type {AikidoTool[]} + */ export const knownAikidoTools = [ { tool: "npm", aikidoCommand: "aikido-npm" }, { tool: "npx", aikidoCommand: "aikido-npx" }, @@ -30,6 +39,11 @@ export function getPackageManagerList() { return `${tools.join(", ")}, and ${lastTool} commands`; } +/** + * @param {string} executableName + * + * @returns {boolean} + */ export function doesExecutableExistOnSystem(executableName) { if (os.platform() === "win32") { const result = spawnSync("where", [executableName], { stdio: "ignore" }); @@ -40,6 +54,13 @@ export function doesExecutableExistOnSystem(executableName) { } } +/** + * @param {string} filePath + * @param {RegExp} pattern + * @param {string} [eol] + * + * @returns {void} + */ export function removeLinesMatchingPattern(filePath, pattern, eol) { if (!fs.existsSync(filePath)) { return; @@ -54,6 +75,12 @@ export function removeLinesMatchingPattern(filePath, pattern, eol) { } const maxLineLength = 100; + +/** + * @param {string} line + * @param {RegExp} pattern + * @returns {boolean} + */ function shouldRemoveLine(line, pattern) { const isPatternMatch = pattern.test(line); @@ -82,7 +109,14 @@ function shouldRemoveLine(line, pattern) { return true; } -export function addLineToFile(filePath, line, eol) { +/** + * @param {string} filePath + * @param {string} line + * @param {string} [eol] + * + * @returns {void} + */ +export function addLineToFile(filePath, line, eol ) { createFileIfNotExists(filePath); eol = eol || os.EOL; @@ -92,6 +126,11 @@ export function addLineToFile(filePath, line, eol) { fs.writeFileSync(filePath, updatedContent, "utf-8"); } +/** + * @param {string} filePath + * + * @returns {void} + */ function createFileIfNotExists(filePath) { if (fs.existsSync(filePath)) { return; diff --git a/packages/safe-chain/src/shell-integration/setup-ci.js b/packages/safe-chain/src/shell-integration/setup-ci.js index 0449ac4..8dd0e4d 100644 --- a/packages/safe-chain/src/shell-integration/setup-ci.js +++ b/packages/safe-chain/src/shell-integration/setup-ci.js @@ -28,6 +28,11 @@ export async function setupCi() { ui.writeInformation(`Added shims directory to PATH for CI environments.`); } +/** + * @param {string} shimsDir + * + * @returns {void} + */ function createUnixShims(shimsDir) { // Read the template file const __filename = fileURLToPath(import.meta.url); @@ -64,6 +69,11 @@ function createUnixShims(shimsDir) { ); } +/** + * @param {string} shimsDir + * + * @returns {void} + */ function createWindowsShims(shimsDir) { // Read the template file const __filename = fileURLToPath(import.meta.url); @@ -97,6 +107,11 @@ function createWindowsShims(shimsDir) { ); } +/** + * @param {string} shimsDir + * + * @returns {void} + */ function createShims(shimsDir) { if (os.platform() === "win32") { createWindowsShims(shimsDir); @@ -105,6 +120,11 @@ function createShims(shimsDir) { } } +/** + * @param {string} shimsDir + * + * @returns {void} + */ function modifyPathForCi(shimsDir) { if (process.env.GITHUB_PATH) { // In GitHub Actions, append the shims directory to GITHUB_PATH diff --git a/packages/safe-chain/src/shell-integration/setup.js b/packages/safe-chain/src/shell-integration/setup.js index afa96e8..7185c5a 100644 --- a/packages/safe-chain/src/shell-integration/setup.js +++ b/packages/safe-chain/src/shell-integration/setup.js @@ -43,7 +43,7 @@ export async function setup() { ui.emptyLine(); ui.writeInformation(`Please restart your terminal to apply the changes.`); } - } catch (error) { + } catch (/** @type {any} */ error) { ui.writeError( `Failed to set up shell aliases: ${error.message}. Please check your shell configuration.` ); @@ -53,6 +53,7 @@ export async function setup() { /** * Calls the setup function for the given shell and reports the result. + * @param {import("./shellDetection.js").Shell} shell */ function setupShell(shell) { let success = false; @@ -60,7 +61,7 @@ function setupShell(shell) { try { shell.teardown(knownAikidoTools); // First, tear down to prevent duplicate aliases success = shell.setup(knownAikidoTools); - } catch (err) { + } catch (/** @type {any} */ err) { success = false; error = err; } diff --git a/packages/safe-chain/src/shell-integration/shellDetection.js b/packages/safe-chain/src/shell-integration/shellDetection.js index d868f6f..701570a 100644 --- a/packages/safe-chain/src/shell-integration/shellDetection.js +++ b/packages/safe-chain/src/shell-integration/shellDetection.js @@ -5,6 +5,17 @@ import windowsPowershell from "./supported-shells/windowsPowershell.js"; import fish from "./supported-shells/fish.js"; import { ui } from "../environment/userInteraction.js"; +/** + * @typedef Shell + * @property {string} name + * @property {() => boolean} isInstalled + * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} setup + * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} teardown + */ + +/** + * @returns {Shell[]} + */ export function detectShells() { let possibleShells = [zsh, bash, powershell, windowsPowershell, fish]; let availableShells = []; @@ -15,7 +26,7 @@ export function detectShells() { availableShells.push(shell); } } - } catch (error) { + } catch (/** @type {any} */ error) { ui.writeError( `We were not able to detect which shells are installed on your system. Please check your shell configuration. Error: ${error.message}` ); diff --git a/packages/safe-chain/src/shell-integration/supported-shells/bash.js b/packages/safe-chain/src/shell-integration/supported-shells/bash.js index 6038f95..a2a3739 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/bash.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/bash.js @@ -15,6 +15,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -57,13 +62,18 @@ function getStartupFile() { }).trim(); return windowsFixPath(path); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); } } +/** + * @param {string} path + * + * @returns {string} + */ function windowsFixPath(path) { try { if (os.platform() !== "win32") { @@ -93,6 +103,11 @@ function hasCygpath() { } } +/** + * @param {string} path + * + * @returns {string} + */ function cygpathw(path) { try { var result = spawnSync("cygpath", ["-w", path], { @@ -108,6 +123,9 @@ function cygpathw(path) { } } +/** + * @type {import("../shellDetection.js").Shell} + */ export default { name: shellName, isInstalled, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/fish.js b/packages/safe-chain/src/shell-integration/supported-shells/fish.js index 4c39ba6..0af6ae3 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/fish.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/fish.js @@ -14,6 +14,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -54,13 +59,16 @@ function getStartupFile() { encoding: "utf8", shell: executableName, }).trim(); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); } } +/** + * @type {import("../shellDetection.js").Shell} + */ export default { name: shellName, isInstalled, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js index 47524c2..8cec258 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/powershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/powershell.js @@ -13,6 +13,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -50,13 +55,16 @@ function getStartupFile() { encoding: "utf8", shell: executableName, }).trim(); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); } } +/** + * @type {import("../shellDetection.js").Shell} + */ export default { name: shellName, isInstalled, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js index 03ff7f8..e554a32 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/windowsPowershell.js @@ -13,6 +13,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -50,13 +55,16 @@ function getStartupFile() { encoding: "utf8", shell: executableName, }).trim(); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); } } +/** + * @type {import("../shellDetection.js").Shell} + */ export default { name: shellName, isInstalled, diff --git a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js index b90f769..fc2b807 100644 --- a/packages/safe-chain/src/shell-integration/supported-shells/zsh.js +++ b/packages/safe-chain/src/shell-integration/supported-shells/zsh.js @@ -14,6 +14,11 @@ function isInstalled() { return doesExecutableExistOnSystem(executableName); } +/** + * @param {import("../helpers.js").AikidoTool[]} tools + * + * @returns {boolean} + */ function teardown(tools) { const startupFile = getStartupFile(); @@ -54,7 +59,7 @@ function getStartupFile() { encoding: "utf8", shell: executableName, }).trim(); - } catch (error) { + } catch (/** @type {any} */ error) { throw new Error( `Command failed: ${startupFileCommand}. Error: ${error.message}` ); diff --git a/packages/safe-chain/src/shell-integration/teardown.js b/packages/safe-chain/src/shell-integration/teardown.js index d6b1277..bc83b48 100644 --- a/packages/safe-chain/src/shell-integration/teardown.js +++ b/packages/safe-chain/src/shell-integration/teardown.js @@ -3,6 +3,9 @@ import { ui } from "../environment/userInteraction.js"; import { detectShells } from "./shellDetection.js"; import { knownAikidoTools, getPackageManagerList } from "./helpers.js"; +/** + * @returns {Promise} + */ export async function teardown() { ui.writeInformation( chalk.bold("Removing shell aliases.") + @@ -52,7 +55,7 @@ export async function teardown() { ui.emptyLine(); ui.writeInformation(`Please restart your terminal to apply the changes.`); } - } catch (error) { + } catch (/** @type {any} */ error) { ui.writeError( `Failed to remove shell aliases: ${error.message}. Please check your shell configuration.` ); diff --git a/packages/safe-chain/src/utils/safeSpawn.js b/packages/safe-chain/src/utils/safeSpawn.js index c398ac2..489d070 100644 --- a/packages/safe-chain/src/utils/safeSpawn.js +++ b/packages/safe-chain/src/utils/safeSpawn.js @@ -1,6 +1,11 @@ import { spawn, execSync } from "child_process"; import os from "os"; +/** + * @param {string} arg + * + * @returns {string} + */ function sanitizeShellArgument(arg) { // If argument contains shell metacharacters, wrap in double quotes // and escape characters that are special even inside double quotes @@ -11,6 +16,11 @@ function sanitizeShellArgument(arg) { return arg; } +/** + * @param {string} arg + * + * @returns {boolean} + */ function hasShellMetaChars(arg) { // Shell metacharacters that need escaping // These characters have special meaning in shells and need to be quoted @@ -20,12 +30,23 @@ function hasShellMetaChars(arg) { return shellMetaChars.test(arg); } +/** + * @param {string} arg + * + * @returns {string} + */ function escapeDoubleQuoteContent(arg) { // Escape special characters for shell safety // This escapes ", $, `, and \ by prefixing them with a backslash return arg.replace(/(["`$\\])/g, "\\$1"); } +/** + * @param {string} command + * @param {string[]} args + * + * @returns {string} + */ function buildCommand(command, args) { if (args.length === 0) { return command; @@ -36,11 +57,17 @@ function buildCommand(command, args) { return `${command} ${escapedArgs.join(" ")}`; } +/** + * @param {string} command + * + * @returns {string} + */ function resolveCommandPath(command) { // command will be "npm", "yarn", etc. // Use 'command -v' to find the full path const fullPath = execSync(`command -v ${command}`, { encoding: "utf8", + // @ts-expect-error shell is a string option shell: true, }).trim(); @@ -51,6 +78,13 @@ function resolveCommandPath(command) { return fullPath; } +/** + * @param {string} command + * @param {string[]} args + * @param {import("child_process").SpawnOptions} options + * + * @returns {Promise<{status: number, stdout: string, stderr: string}>} + */ export async function safeSpawn(command, args, options = {}) { // The command is always one of our supported package managers. // It should always be alphanumeric or _ or - @@ -87,6 +121,7 @@ export async function safeSpawn(command, args, options = {}) { child.on("close", (code) => { resolve({ + // @ts-expect-error code can be null status: code, stdout: stdout, stderr: stderr, diff --git a/packages/safe-chain/tsconfig.json b/packages/safe-chain/tsconfig.json new file mode 100644 index 0000000..c357bb1 --- /dev/null +++ b/packages/safe-chain/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "strict": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "allowJs": true, + "checkJs": true, + "noEmit": true, + "resolveJsonModule": true + }, + "include": [ + "src/**/*.js", + "bin/**/*.js" + ], + "exclude": [ + "node_modules", + "src/**/*.spec.js" + ] +} From 5adfb36629f22550475a3bbe746dc17cb6c781d3 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:07:31 +0100 Subject: [PATCH 02/20] Run typecheck as part of CI --- .github/workflows/test-on-pr.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test-on-pr.yml b/.github/workflows/test-on-pr.yml index 85d6aba..f8087ef 100644 --- a/.github/workflows/test-on-pr.yml +++ b/.github/workflows/test-on-pr.yml @@ -31,6 +31,9 @@ jobs: - name: Run linting run: npm run lint + - name: Type check + run: npm run typecheck --workspace=packages/safe-chain + - name: Create package tarball run: npm pack --workspace=packages/safe-chain From 6f962a9299306da6195da4c2365dd12fac9fbdb2 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:09:08 +0100 Subject: [PATCH 03/20] Use Node.js 18 types --- package-lock.json | 17 ++++++++++++++++- packages/safe-chain/package.json | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b0b5cf..f258df3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2091,7 +2091,7 @@ }, "devDependencies": { "@types/make-fetch-happen": "^10.0.4", - "@types/node": "^24.9.2", + "@types/node": "^18.19.130", "@types/node-forge": "^1.3.14", "@types/npm-registry-fetch": "^8.0.9", "@types/semver": "^7.7.1", @@ -2109,6 +2109,21 @@ "bun": ">=1.2.21" } }, + "packages/safe-chain/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "packages/safe-chain/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "test/e2e": { "name": "@aikidosec/safe-chain-e2e-tests", "version": "1.0.0", diff --git a/packages/safe-chain/package.json b/packages/safe-chain/package.json index 4e55840..e201b0f 100644 --- a/packages/safe-chain/package.json +++ b/packages/safe-chain/package.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@types/make-fetch-happen": "^10.0.4", - "@types/node": "^24.9.2", + "@types/node": "^18.19.130", "@types/npm-registry-fetch": "^8.0.9", "@types/semver": "^7.7.1", "@types/node-forge": "^1.3.14", From 4f148593516e3d8ad94385818de2f016e1216c56 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:24:57 +0100 Subject: [PATCH 04/20] Fix check --- packages/safe-chain/src/api/npmApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/safe-chain/src/api/npmApi.js b/packages/safe-chain/src/api/npmApi.js index c8e2218..c03abef 100644 --- a/packages/safe-chain/src/api/npmApi.js +++ b/packages/safe-chain/src/api/npmApi.js @@ -17,7 +17,7 @@ export async function resolvePackageVersion(packageName, versionRange) { } const packageInfo = ( - /** @type {{"dist-tags"?: Record} | null} */ + /** @type {{"dist-tags"?: Record, versions?: Record} | null} */ await getPackageInfo(packageName) ); if (!packageInfo) { @@ -27,7 +27,7 @@ export async function resolvePackageVersion(packageName, versionRange) { } const distTags = packageInfo["dist-tags"]; - if (distTags && isDistTags(distTags)) { + if (distTags && isDistTags(distTags) && distTags[versionRange]) { // If the version range is a dist-tag, return the version associated with that tag // e.g., "latest", "next", etc. return distTags[versionRange]; From 29dd63d1ebdfab45c0a276a0e4373976d341aa8e Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:26:15 +0100 Subject: [PATCH 05/20] Reduce diff --- packages/safe-chain/src/config/configFile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 83bc186..20dabdc 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -113,7 +113,6 @@ function getDatabasePath() { return path.join(aikidoDir, "malwareDatabase.json"); } - function getDatabaseVersionPath() { const aikidoDir = getAikidoDirectory(); return path.join(aikidoDir, "version.txt"); From 484cbcd96058eeed382c597c606d371a79a95b4d Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:28:11 +0100 Subject: [PATCH 06/20] Use @typedef {Object} X MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When you write @typedef {Object} ScanResult, you’re telling both JSDoc and TypeScript’s parser that this typedef represents an object type, not just an abstract name. This is important because it makes tools like IDEs, linters, and TypeScript’s JSDoc inference more reliable. It avoids ambiguity, especially in cases where the typedef might later be confused with something like a primitive, union, or function type. The official TypeScript documentation and the JSDoc spec both show this form as the canonical one for object shapes. --- packages/safe-chain/src/api/aikido.js | 2 +- packages/safe-chain/src/environment/userInteraction.js | 2 +- .../safe-chain/src/packagemanager/currentPackageManager.js | 2 +- .../npm/dependencyScanner/commandArgumentScanner.js | 2 +- packages/safe-chain/src/scanning/audit/index.js | 4 ++-- packages/safe-chain/src/scanning/malwareDatabase.js | 2 +- packages/safe-chain/src/shell-integration/helpers.js | 2 +- packages/safe-chain/src/shell-integration/shellDetection.js | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/safe-chain/src/api/aikido.js b/packages/safe-chain/src/api/aikido.js index 7e786d1..901131c 100644 --- a/packages/safe-chain/src/api/aikido.js +++ b/packages/safe-chain/src/api/aikido.js @@ -4,7 +4,7 @@ const malwareDatabaseUrl = "https://malware-list.aikido.dev/malware_predictions.json"; /** - * @typedef MalwarePackage + * @typedef {Object} MalwarePackage * @property {string} package_name * @property {string} version * @property {string} reason diff --git a/packages/safe-chain/src/environment/userInteraction.js b/packages/safe-chain/src/environment/userInteraction.js index 43f5312..3f690fb 100644 --- a/packages/safe-chain/src/environment/userInteraction.js +++ b/packages/safe-chain/src/environment/userInteraction.js @@ -60,7 +60,7 @@ function writeExitWithoutInstallingMaliciousPackages() { } /** - * @typedef Spinner + * @typedef {Object} Spinner * @property {(message: string) => void} succeed * @property {(message: string) => void} fail * @property {() => void} stop diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index aaf8aae..ef7e9aa 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -18,7 +18,7 @@ const state = { }; /** - * @typedef PackageManager + * @typedef {Object} PackageManager * @property {(args: string[]) => Promise<{ status: number }>} runCommand * @property {(args: string[]) => boolean} isSupportedCommand * @property {(args: string[]) => Promise<{name: string, version: string, type: string}[]>} getDependencyUpdatesForCommand diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js index 51746c4..f19449b 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js @@ -15,7 +15,7 @@ import { hasDryRunArg } from "../utils/npmCommands.js"; */ /** - * @typedef CommandArgumentScanner + * @typedef {Object} CommandArgumentScanner * @property {(args: string[]) => Promise} scan * @property {(args: string[]) => boolean} shouldScan */ diff --git a/packages/safe-chain/src/scanning/audit/index.js b/packages/safe-chain/src/scanning/audit/index.js index 8eea526..b6dfcc5 100644 --- a/packages/safe-chain/src/scanning/audit/index.js +++ b/packages/safe-chain/src/scanning/audit/index.js @@ -4,14 +4,14 @@ import { } from "../malwareDatabase.js"; /** - * @typedef PackageChange + * @typedef {Object} PackageChange * @property {string} name * @property {string} version * @property {string} type */ /** - * @typedef AuditResult + * @typedef {Object} AuditResult * @property {PackageChange[]} allowedChanges * @property {(PackageChange & {reason: string})[]} disallowedChanges * @property {boolean} isAllowed diff --git a/packages/safe-chain/src/scanning/malwareDatabase.js b/packages/safe-chain/src/scanning/malwareDatabase.js index 481ff7d..b54846e 100644 --- a/packages/safe-chain/src/scanning/malwareDatabase.js +++ b/packages/safe-chain/src/scanning/malwareDatabase.js @@ -9,7 +9,7 @@ import { import { ui } from "../environment/userInteraction.js"; /** - * @typedef MalwareDatabase + * @typedef {Object} MalwareDatabase * @property {function(string, string): string} getPackageStatus * @property {function(string, string): boolean} isMalware */ diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 697f2cb..1732228 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -4,7 +4,7 @@ import fs from "fs"; import path from "path"; /** - * @typedef AikidoTool + * @typedef {Object} AikidoTool * @property {string} tool * @property {string} aikidoCommand */ diff --git a/packages/safe-chain/src/shell-integration/shellDetection.js b/packages/safe-chain/src/shell-integration/shellDetection.js index 701570a..9e0f110 100644 --- a/packages/safe-chain/src/shell-integration/shellDetection.js +++ b/packages/safe-chain/src/shell-integration/shellDetection.js @@ -6,7 +6,7 @@ import fish from "./supported-shells/fish.js"; import { ui } from "../environment/userInteraction.js"; /** - * @typedef Shell + * @typedef {Object} Shell * @property {string} name * @property {() => boolean} isInstalled * @property {(tools: import("./helpers.js").AikidoTool[]) => boolean} setup From 86a2b8c2a715f7a353640a9b390e00cafb46d19f Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:44:48 +0100 Subject: [PATCH 07/20] Fix lint --- .../npx/dependencyScanner/commandArgumentScanner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js index 7f4da9f..689e3f8 100644 --- a/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npx/dependencyScanner/commandArgumentScanner.js @@ -7,7 +7,7 @@ import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArgument export function commandArgumentScanner() { return { scan: (args) => scanDependencies(args), - shouldScan: (args) => true, // all npx commands need to be scanned, npx doesn't have dry-run + shouldScan: () => true, // all npx commands need to be scanned, npx doesn't have dry-run }; } From e164eb8b959c0064ad338f2e36fccd8de6123bc4 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sat, 1 Nov 2025 13:47:13 +0100 Subject: [PATCH 08/20] Reduce diff --- packages/safe-chain/src/shell-integration/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 1732228..89321b1 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -116,7 +116,7 @@ function shouldRemoveLine(line, pattern) { * * @returns {void} */ -export function addLineToFile(filePath, line, eol ) { +export function addLineToFile(filePath, line, eol) { createFileIfNotExists(filePath); eol = eol || os.EOL; From b489fe822cf67a188485f1ffc7767c1282d7b5b2 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 2 Nov 2025 15:29:23 +0100 Subject: [PATCH 09/20] Example of mistake --- packages/safe-chain/src/shell-integration/helpers.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 89321b1..e0955ff 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,6 +3,12 @@ import * as os from "os"; import fs from "fs"; import path from "path"; +function mistakeHere() { + os.EOLL; +} + +mistakeHere(); + /** * @typedef {Object} AikidoTool * @property {string} tool From 0cfce2d436b28f8f83d4139bb18faa8372460f9a Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 2 Nov 2025 15:29:36 +0100 Subject: [PATCH 10/20] Revert "Example of mistake" This reverts commit b489fe822cf67a188485f1ffc7767c1282d7b5b2. --- packages/safe-chain/src/shell-integration/helpers.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index e0955ff..89321b1 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,12 +3,6 @@ import * as os from "os"; import fs from "fs"; import path from "path"; -function mistakeHere() { - os.EOLL; -} - -mistakeHere(); - /** * @typedef {Object} AikidoTool * @property {string} tool From 1724e0b1990b157d1d2a847af94f9d3df408bf22 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 2 Nov 2025 15:31:02 +0100 Subject: [PATCH 11/20] Introduce mistake that passes linter --- packages/safe-chain/src/shell-integration/helpers.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 89321b1..3949886 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,6 +3,13 @@ import * as os from "os"; import fs from "fs"; import path from "path"; +function mistakeHere() { + return os.EOLL; +} + +const abc = mistakeHere(); +console.log(abc); + /** * @typedef {Object} AikidoTool * @property {string} tool From e8e7c85c62b4eaaceb5cf1a71debac49096c784b Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Sun, 2 Nov 2025 15:31:23 +0100 Subject: [PATCH 12/20] Revert "Introduce mistake that passes linter" This reverts commit 1724e0b1990b157d1d2a847af94f9d3df408bf22. --- packages/safe-chain/src/shell-integration/helpers.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/safe-chain/src/shell-integration/helpers.js b/packages/safe-chain/src/shell-integration/helpers.js index 3949886..89321b1 100644 --- a/packages/safe-chain/src/shell-integration/helpers.js +++ b/packages/safe-chain/src/shell-integration/helpers.js @@ -3,13 +3,6 @@ import * as os from "os"; import fs from "fs"; import path from "path"; -function mistakeHere() { - return os.EOLL; -} - -const abc = mistakeHere(); -console.log(abc); - /** * @typedef {Object} AikidoTool * @property {string} tool From 49d31049ac2ca3e3132b7b36187282cc891e9c49 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:04:12 +0100 Subject: [PATCH 13/20] Revert code Let's do it in a separate PR --- packages/safe-chain/src/config/configFile.js | 30 ++------------------ 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 20dabdc..a11acc2 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -7,34 +7,10 @@ import { ui } from "../environment/userInteraction.js"; * @returns {number} */ export function getScanTimeout() { - if (process.env.AIKIDO_SCAN_TIMEOUT_MS) { - const timeout = parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS); - if (!isNaN(timeout) && timeout >= 0) { - return timeout; - } - } + const config = /** @type {{scanTimeout: number}} */ (readConfigFile()); - const config = readConfigFile(); - - if (hasScanTimeout(config) && config.scanTimeout >= 0) { - return config.scanTimeout; - } - - return 10000; // Default to 10 seconds -} - -/** - * @param {unknown} config - * - * @returns {config is {scanTimeout: number}} - */ -function hasScanTimeout(config) { - return ( - typeof config === "object" && - config !== null && - "scanTimeout" in config && - typeof config.scanTimeout === "number" - ); + // @ts-expect-error values of process.env can be string | undefined + return parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds } /** From ad9551ca6dc70cd206816905822b1cfdd4301112 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:26:10 +0100 Subject: [PATCH 14/20] Improve types and remove `async` --- .../src/packagemanager/bun/createBunPackageManager.js | 4 ++-- .../src/packagemanager/currentPackageManager.js | 9 ++++++++- .../src/packagemanager/npm/createPackageManager.js | 2 +- .../npm/dependencyScanner/commandArgumentScanner.js | 2 +- .../packagemanager/npm/dependencyScanner/nullScanner.js | 2 +- .../src/packagemanager/pnpm/createPackageManager.js | 2 +- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js index 88c84f5..9716261 100644 --- a/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js +++ b/packages/safe-chain/src/packagemanager/bun/createBunPackageManager.js @@ -12,7 +12,7 @@ export function createBunPackageManager() { // For bun, we use the proxy-only approach to block package downloads, // so we don't need to analyze commands. isSupportedCommand: () => false, - getDependencyUpdatesForCommand: async () => [], + getDependencyUpdatesForCommand: () => [], }; } @@ -26,7 +26,7 @@ export function createBunxPackageManager() { // For bunx, we use the proxy-only approach to block package downloads, // so we don't need to analyze commands. isSupportedCommand: () => false, - getDependencyUpdatesForCommand: async () => [], + getDependencyUpdatesForCommand: () => [], }; } diff --git a/packages/safe-chain/src/packagemanager/currentPackageManager.js b/packages/safe-chain/src/packagemanager/currentPackageManager.js index ef7e9aa..390d4d1 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -17,11 +17,18 @@ const state = { packageManagerName: null, }; +/** + * @typedef {Object} GetDependencyUpdatesResult + * @property {string} name + * @property {string} version + * @property {string} type + */ + /** * @typedef {Object} PackageManager * @property {(args: string[]) => Promise<{ status: number }>} runCommand * @property {(args: string[]) => boolean} isSupportedCommand - * @property {(args: string[]) => Promise<{name: string, version: string, type: string}[]>} getDependencyUpdatesForCommand + * @property {(args: string[]) => Promise | GetDependencyUpdatesResult[]} getDependencyUpdatesForCommand */ /** diff --git a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js index 465bf60..fa72276 100644 --- a/packages/safe-chain/src/packagemanager/npm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/npm/createPackageManager.js @@ -28,7 +28,7 @@ export function createNpmPackageManager() { /** * @param {string[]} args * - * @returns {Promise<{name: string, version: string, type: string}[]>} + * @returns {ReturnType} */ function getDependencyUpdatesForCommand(args) { const scanner = findDependencyScannerForCommand( diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js index f19449b..c4f6bb6 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/commandArgumentScanner.js @@ -16,7 +16,7 @@ import { hasDryRunArg } from "../utils/npmCommands.js"; /** * @typedef {Object} CommandArgumentScanner - * @property {(args: string[]) => Promise} scan + * @property {(args: string[]) => Promise | ScanResult[]} scan * @property {(args: string[]) => boolean} shouldScan */ diff --git a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js index 449fed4..5c1d3bd 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js @@ -3,7 +3,7 @@ */ export function nullScanner() { return { - scan: async () => [], + scan: () => [], shouldScan: () => false, }; } diff --git a/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js b/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js index 193470b..c3046c8 100644 --- a/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js +++ b/packages/safe-chain/src/packagemanager/pnpm/createPackageManager.js @@ -41,7 +41,7 @@ export function createPnpxPackageManager() { /** * @param {string[]} args * @param {boolean} isPnpx - * @returns {Promise} + * @returns {ReturnType} */ function getDependencyUpdatesForCommand(args, isPnpx) { if (isPnpx) { From c3a62826d40b11974a229f5a86d3dfe3e7e12f74 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:28:24 +0100 Subject: [PATCH 15/20] Make prop optional --- packages/safe-chain/src/config/configFile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index a11acc2..7273f7a 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -7,7 +7,7 @@ import { ui } from "../environment/userInteraction.js"; * @returns {number} */ export function getScanTimeout() { - const config = /** @type {{scanTimeout: number}} */ (readConfigFile()); + const config = /** @type {{scanTimeout?: number}} */ (readConfigFile()); // @ts-expect-error values of process.env can be string | undefined return parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds From 910276deebd217a123cf5edcf887d173c89007e6 Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:30:21 +0100 Subject: [PATCH 16/20] Fix type --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index be2b357..ff80087 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -78,7 +78,7 @@ function createHttpsServer(hostname, isAllowed) { /** * @param {string} url - * @returns {*|string} + * @returns {string} */ function getRequestPathAndQuery(url) { if (url.startsWith("http://") || url.startsWith("https://")) { From 855f6a417f304fa63cb538629e160b7060cbb27b Mon Sep 17 00:00:00 2001 From: Hans Ott Date: Mon, 3 Nov 2025 11:31:04 +0100 Subject: [PATCH 17/20] Use original notation --- packages/safe-chain/src/registryProxy/mitmRequestHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js index ff80087..5bbf38a 100644 --- a/packages/safe-chain/src/registryProxy/mitmRequestHandler.js +++ b/packages/safe-chain/src/registryProxy/mitmRequestHandler.js @@ -133,7 +133,7 @@ function createProxyRequest(hostname, req, res) { }; if (options.headers && "host" in options.headers) { - delete options.headers["host"]; + delete options.headers.host; } const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy; From 5304a7744a67da875ed73a1ce744f03fccbfc63a Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 14:41:29 +0100 Subject: [PATCH 18/20] Add better error handling, tests and type checks for configFile.js --- packages/safe-chain/src/config/configFile.js | 30 +++- .../safe-chain/src/config/configFile.spec.js | 140 ++++++++++++++++++ 2 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 packages/safe-chain/src/config/configFile.spec.js diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index 7273f7a..ab96bb1 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -3,14 +3,32 @@ import path from "path"; import os from "os"; import { ui } from "../environment/userInteraction.js"; +/** + * @typedef {Object} SafeChainConfig + * @property {any} scanTimeout // This should be a number + */ + /** * @returns {number} */ export function getScanTimeout() { - const config = /** @type {{scanTimeout?: number}} */ (readConfigFile()); + const config = readConfigFile(); - // @ts-expect-error values of process.env can be string | undefined - return parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds + if (process.env.AIKIDO_SCAN_TIMEOUT_MS) { + const scanTimeout = Number(process.env.AIKIDO_SCAN_TIMEOUT_MS); + if (!Number.isNaN(scanTimeout) && scanTimeout > 0) { + return scanTimeout; + } + } + + if (config.scanTimeout) { + const scanTimeout = Number(config.scanTimeout); + if (!Number.isNaN(scanTimeout) && scanTimeout > 0) { + return scanTimeout; + } + } + + return 10000; // Default to 10 seconds } /** @@ -68,13 +86,15 @@ export function readDatabaseFromLocalCache() { } /** - * @returns {unknown} + * @returns {SafeChainConfig} */ function readConfigFile() { const configFilePath = getConfigFilePath(); if (!fs.existsSync(configFilePath)) { - return {}; + return { + scanTimeout: undefined, + }; } const data = fs.readFileSync(configFilePath, "utf8"); diff --git a/packages/safe-chain/src/config/configFile.spec.js b/packages/safe-chain/src/config/configFile.spec.js new file mode 100644 index 0000000..f52d20b --- /dev/null +++ b/packages/safe-chain/src/config/configFile.spec.js @@ -0,0 +1,140 @@ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert"; +import { getScanTimeout } from "./configFile.js"; +import fs from "fs"; +import path from "path"; +import os from "os"; + +describe("getScanTimeout", () => { + let originalEnv; + let aikidoDir; + let configPath; + let configBackupPath; + + beforeEach(() => { + // Save original environment + originalEnv = process.env.AIKIDO_SCAN_TIMEOUT_MS; + + // Use the actual .aikido directory + aikidoDir = path.join(os.homedir(), ".aikido"); + configPath = path.join(aikidoDir, "config.json"); + configBackupPath = path.join(aikidoDir, "config.json.backup"); + + // Backup existing config if it exists + if (fs.existsSync(configPath)) { + fs.copyFileSync(configPath, configBackupPath); + } + }); + + afterEach(() => { + // Restore original environment + if (originalEnv !== undefined) { + process.env.AIKIDO_SCAN_TIMEOUT_MS = originalEnv; + } else { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + } + + // Restore original config file + if (fs.existsSync(configBackupPath)) { + fs.copyFileSync(configBackupPath, configPath); + fs.unlinkSync(configBackupPath); + } else if (fs.existsSync(configPath)) { + fs.unlinkSync(configPath); + } + }); + + it("should return default timeout of 10000ms when no config or env var is set", () => { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + if (fs.existsSync(configPath)) { + fs.unlinkSync(configPath); + } + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 10000); + }); + + it("should return timeout from config file when set", () => { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 5000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 5000); + }); + + it("should prioritize environment variable over config file", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 5000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 20000); + }); + + it("should handle invalid environment variable and fall back to config", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 7000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 7000); + }); + + it("should ignore zero and negative values and fall back to default", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "0"; + + let timeout = getScanTimeout(); + assert.strictEqual(timeout, 10000); + + process.env.AIKIDO_SCAN_TIMEOUT_MS = "-5000"; + + timeout = getScanTimeout(); + assert.strictEqual(timeout, 10000); + }); + + it("should ignore textual non-numeric values in environment variable and fall back to config", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 8000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 8000); + }); + + it("should ignore textual non-numeric values in config file and fall back to default", () => { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "slow" })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 10000); + }); + + it("should ignore textual non-numeric values in both env and config, fall back to default", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "medium" })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 10000); + }); + + it("should ignore mixed alphanumeric strings in environment variable", () => { + process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms"; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 6000 })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 6000); + }); + + it("should ignore mixed alphanumeric strings in config file", () => { + delete process.env.AIKIDO_SCAN_TIMEOUT_MS; + fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "3000ms" })); + + const timeout = getScanTimeout(); + + assert.strictEqual(timeout, 10000); + }); +}); From 1e7cd74364b2c80336d659eca26c99c8db169841 Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 14:49:44 +0100 Subject: [PATCH 19/20] Mock filesystem in configFile.spec.js --- packages/safe-chain/src/config/configFile.js | 5 +- .../safe-chain/src/config/configFile.spec.js | 102 ++++++++++++------ 2 files changed, 71 insertions(+), 36 deletions(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index ab96bb1..cf36b12 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -5,7 +5,10 @@ import { ui } from "../environment/userInteraction.js"; /** * @typedef {Object} SafeChainConfig - * @property {any} scanTimeout // This should be a number + * + * This should be a number, but can be anything because it is user-input. + * We cannot trust the input and should add the necessary validations. + * @property {any} scanTimeout */ /** diff --git a/packages/safe-chain/src/config/configFile.spec.js b/packages/safe-chain/src/config/configFile.spec.js index f52d20b..8ec980c 100644 --- a/packages/safe-chain/src/config/configFile.spec.js +++ b/packages/safe-chain/src/config/configFile.spec.js @@ -1,29 +1,32 @@ -import { describe, it, beforeEach, afterEach } from "node:test"; +import { describe, it, beforeEach, afterEach, mock } from "node:test"; import assert from "node:assert"; -import { getScanTimeout } from "./configFile.js"; -import fs from "fs"; -import path from "path"; -import os from "os"; describe("getScanTimeout", () => { let originalEnv; - let aikidoDir; - let configPath; - let configBackupPath; + let fsMock; + let getScanTimeout; - beforeEach(() => { + beforeEach(async () => { // Save original environment originalEnv = process.env.AIKIDO_SCAN_TIMEOUT_MS; - // Use the actual .aikido directory - aikidoDir = path.join(os.homedir(), ".aikido"); - configPath = path.join(aikidoDir, "config.json"); - configBackupPath = path.join(aikidoDir, "config.json.backup"); + // Mock fs module + fsMock = { + existsSync: mock.fn(() => false), + readFileSync: mock.fn(() => "{}"), + writeFileSync: mock.fn(), + mkdirSync: mock.fn(), + }; - // Backup existing config if it exists - if (fs.existsSync(configPath)) { - fs.copyFileSync(configPath, configBackupPath); - } + mock.module("fs", { + namedExports: fsMock, + }); + + // Re-import the module to get the mocked version + const configFileModule = await import( + `./configFile.js?update=${Date.now()}` + ); + getScanTimeout = configFileModule.getScanTimeout; }); afterEach(() => { @@ -34,20 +37,14 @@ describe("getScanTimeout", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; } - // Restore original config file - if (fs.existsSync(configBackupPath)) { - fs.copyFileSync(configBackupPath, configPath); - fs.unlinkSync(configBackupPath); - } else if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath); - } + // Reset all mocks + mock.restoreAll(); }); it("should return default timeout of 10000ms when no config or env var is set", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath); - } + // Mock: config file doesn't exist + fsMock.existsSync.mock.mockImplementation(() => false); const timeout = getScanTimeout(); @@ -56,7 +53,11 @@ describe("getScanTimeout", () => { it("should return timeout from config file when set", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 5000 })); + // Mock: config file exists with scanTimeout: 5000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 5000 }) + ); const timeout = getScanTimeout(); @@ -65,7 +66,11 @@ describe("getScanTimeout", () => { it("should prioritize environment variable over config file", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 5000 })); + // Mock: config file exists with scanTimeout: 5000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 5000 }) + ); const timeout = getScanTimeout(); @@ -74,7 +79,11 @@ describe("getScanTimeout", () => { it("should handle invalid environment variable and fall back to config", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 7000 })); + // Mock: config file exists with scanTimeout: 7000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 7000 }) + ); const timeout = getScanTimeout(); @@ -82,6 +91,9 @@ describe("getScanTimeout", () => { }); it("should ignore zero and negative values and fall back to default", () => { + // Mock: config file doesn't exist + fsMock.existsSync.mock.mockImplementation(() => false); + process.env.AIKIDO_SCAN_TIMEOUT_MS = "0"; let timeout = getScanTimeout(); @@ -95,7 +107,11 @@ describe("getScanTimeout", () => { it("should ignore textual non-numeric values in environment variable and fall back to config", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 8000 })); + // Mock: config file exists with scanTimeout: 8000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 8000 }) + ); const timeout = getScanTimeout(); @@ -104,7 +120,11 @@ describe("getScanTimeout", () => { it("should ignore textual non-numeric values in config file and fall back to default", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "slow" })); + // Mock: config file exists with scanTimeout: "slow" + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: "slow" }) + ); const timeout = getScanTimeout(); @@ -113,7 +133,11 @@ describe("getScanTimeout", () => { it("should ignore textual non-numeric values in both env and config, fall back to default", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "medium" })); + // Mock: config file exists with scanTimeout: "medium" + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: "medium" }) + ); const timeout = getScanTimeout(); @@ -122,7 +146,11 @@ describe("getScanTimeout", () => { it("should ignore mixed alphanumeric strings in environment variable", () => { process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms"; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: 6000 })); + // Mock: config file exists with scanTimeout: 6000 + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: 6000 }) + ); const timeout = getScanTimeout(); @@ -131,7 +159,11 @@ describe("getScanTimeout", () => { it("should ignore mixed alphanumeric strings in config file", () => { delete process.env.AIKIDO_SCAN_TIMEOUT_MS; - fs.writeFileSync(configPath, JSON.stringify({ scanTimeout: "3000ms" })); + // Mock: config file exists with scanTimeout: "3000ms" + fsMock.existsSync.mock.mockImplementation(() => true); + fsMock.readFileSync.mock.mockImplementation(() => + JSON.stringify({ scanTimeout: "3000ms" }) + ); const timeout = getScanTimeout(); From 8c872b3861d8dab2ef8fac63b1ac97a5835a8f4c Mon Sep 17 00:00:00 2001 From: Sander Declerck Date: Mon, 3 Nov 2025 14:54:42 +0100 Subject: [PATCH 20/20] Better error handling and extract validation logic to a re-usable function. --- packages/safe-chain/src/config/configFile.js | 31 ++++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/safe-chain/src/config/configFile.js b/packages/safe-chain/src/config/configFile.js index cf36b12..901e7d7 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -18,15 +18,15 @@ export function getScanTimeout() { const config = readConfigFile(); if (process.env.AIKIDO_SCAN_TIMEOUT_MS) { - const scanTimeout = Number(process.env.AIKIDO_SCAN_TIMEOUT_MS); - if (!Number.isNaN(scanTimeout) && scanTimeout > 0) { + const scanTimeout = validateTimeout(process.env.AIKIDO_SCAN_TIMEOUT_MS); + if (scanTimeout != null) { return scanTimeout; } } if (config.scanTimeout) { - const scanTimeout = Number(config.scanTimeout); - if (!Number.isNaN(scanTimeout) && scanTimeout > 0) { + const scanTimeout = validateTimeout(config.scanTimeout); + if (scanTimeout != null) { return scanTimeout; } } @@ -34,6 +34,19 @@ export function getScanTimeout() { return 10000; // Default to 10 seconds } +/** + * + * @param {any} value + * @returns {number?} + */ +function validateTimeout(value) { + const timeout = Number(value); + if (!Number.isNaN(timeout) && timeout > 0) { + return timeout; + } + return null; +} + /** * @param {import("../api/aikido.js").MalwarePackage[]} data * @param {string | number} version @@ -100,8 +113,14 @@ function readConfigFile() { }; } - const data = fs.readFileSync(configFilePath, "utf8"); - return JSON.parse(data); + try { + const data = fs.readFileSync(configFilePath, "utf8"); + return JSON.parse(data); + } catch { + return { + scanTimeout: undefined, + }; + } } /**