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 diff --git a/package-lock.json b/package-lock.json index 88e9fb5..f258df3 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": "^18.19.130", + "@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": { @@ -1752,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-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..e201b0f 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": "^18.19.130", + "@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..901131c 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 {Object} 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..c03abef 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, versions?: 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) && 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]; @@ -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..7273f7a 100644 --- a/packages/safe-chain/src/config/configFile.js +++ b/packages/safe-chain/src/config/configFile.js @@ -3,13 +3,22 @@ import path from "path"; import os from "os"; import { ui } from "../environment/userInteraction.js"; +/** + * @returns {number} + */ export function getScanTimeout() { - const config = readConfigFile(); - return ( - parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds - ); + 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 } +/** + * @param {import("../api/aikido.js").MalwarePackage[]} data + * @param {string | number} version + * + * @returns {void} + */ export function writeDatabaseToLocalCache(data, version) { try { const databasePath = getDatabasePath(); @@ -24,6 +33,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 +67,9 @@ export function readDatabaseFromLocalCache() { } } +/** + * @returns {unknown} + */ function readConfigFile() { const configFilePath = getConfigFilePath(); @@ -66,6 +81,9 @@ function readConfigFile() { return JSON.parse(data); } +/** + * @returns {string} + */ function getDatabasePath() { const aikidoDir = getAikidoDirectory(); return path.join(aikidoDir, "malwareDatabase.json"); @@ -76,10 +94,16 @@ function getDatabaseVersionPath() { 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..3f690fb 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 {Object} 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..9716261 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), @@ -13,6 +16,9 @@ export function createBunPackageManager() { }; } +/** + * @returns {import("../currentPackageManager.js").PackageManager} + */ export function createBunxPackageManager() { return { runCommand: (args) => runBunCommand("bunx", args), @@ -24,14 +30,20 @@ export function createBunxPackageManager() { }; } +/** + * @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..390d4d1 100644 --- a/packages/safe-chain/src/packagemanager/currentPackageManager.js +++ b/packages/safe-chain/src/packagemanager/currentPackageManager.js @@ -10,10 +10,32 @@ import { } from "./pnpm/createPackageManager.js"; import { createYarnPackageManager } from "./yarn/createPackageManager.js"; +/** + * @type {{packageManagerName: PackageManager | null}} + */ 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 | GetDependencyUpdatesResult[]} 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..fa72276 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 {ReturnType} + */ 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..c4f6bb6 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 {Object} CommandArgumentScanner + * @property {(args: string[]) => Promise | ScanResult[]} 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..5c1d3bd 100644 --- a/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js +++ b/packages/safe-chain/src/packagemanager/npm/dependencyScanner/nullScanner.js @@ -1,3 +1,6 @@ +/** + * @returns {import("./commandArgumentScanner.js").CommandArgumentScanner} + */ export function nullScanner() { return { scan: () => [], 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..689e3f8 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 }; } + +/** + * @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..c3046c8 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 {ReturnType} + */ 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..5bbf38a 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..b6dfcc5 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 {Object} PackageChange + * @property {string} name + * @property {string} version + * @property {string} type + */ + +/** + * @typedef {Object} 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..b54846e 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 {Object} 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..89321b1 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 {Object} 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,6 +109,13 @@ function shouldRemoveLine(line, pattern) { return true; } +/** + * @param {string} filePath + * @param {string} line + * @param {string} [eol] + * + * @returns {void} + */ export function addLineToFile(filePath, line, eol) { createFileIfNotExists(filePath); @@ -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..9e0f110 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 {Object} 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" + ] +}