diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 746a2f1..87f129d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,10 @@ jobs: node-version: 22 cache: npm + - name: Check lockfile registry + # Every resolved URL must point at registry.npmjs.org. + run: npm run check:lockfile + - name: Install dependencies run: npm ci diff --git a/package-lock.json b/package-lock.json index 200f5b0..f67c4bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.10.0", "license": "MIT", "dependencies": { - "simple-git": "^3.22.0" + "simple-git": "^4.0.2" }, "devDependencies": { "@eslint/js": "^9.x", @@ -521,7 +521,7 @@ }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", "dependencies": { "debug": "^4.1.1" @@ -529,7 +529,7 @@ }, "node_modules/@kwsites/promise-deferred": { "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, "node_modules/@nodelib/fs.scandir": { @@ -769,18 +769,18 @@ } }, "node_modules/@simple-git/args-pathspec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", - "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.4.tgz", + "integrity": "sha512-EtMX6XjRWSastG2SdkmSQByPtJ9dx/NIjnHbpQPCYt62j4RmSx5rgLTGpw0YCjF5h191gZOmBfheOT23cRSFdw==", "license": "MIT" }, "node_modules/@simple-git/argv-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", - "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-2.0.1.tgz", + "integrity": "sha512-M++IaVWrN+vYalilOHfwvT2IL3wYZtVEBQpmX6DLSOJLtxbZ+SUeIDE3IiPogzhPACLajwtxfa5rp+ZX1FczzQ==", "license": "MIT", "dependencies": { - "@simple-git/args-pathspec": "^1.0.3" + "@simple-git/args-pathspec": "^1.0.4" } }, "node_modules/@sindresorhus/merge-streams": { @@ -898,7 +898,7 @@ }, "node_modules/@types/node": { "version": "20.19.26", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.26.tgz", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz", "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", "dev": true, "dependencies": { @@ -921,7 +921,7 @@ }, "node_modules/@types/vscode": { "version": "1.106.1", - "resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.106.1.tgz", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.106.1.tgz", "integrity": "sha512-R/HV8u2h8CAddSbX8cjpdd7B8/GnE4UjgjpuGuHcbp1xV6yh4OeqU4L1pKjlwujCrSFS0MOpwJAIs/NexMB1fQ==", "dev": true }, @@ -1872,7 +1872,7 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": { "ms": "^2.1.3" @@ -3601,7 +3601,7 @@ }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mute-stream": { @@ -4519,15 +4519,15 @@ } }, "node_modules/simple-git": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", - "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-4.0.2.tgz", + "integrity": "sha512-l0sIsv9VrPwovKELFDBOj/lhmIcbMvxBk2B+hRhsMM4/VL3gKtuZvYnBNixVo+N/0HnqQx2cBdLvxnAbygHpqA==", "license": "MIT", "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "@simple-git/args-pathspec": "^1.0.3", - "@simple-git/argv-parser": "^1.1.0", + "@simple-git/args-pathspec": "1.0.4", + "@simple-git/argv-parser": "2.0.1", "debug": "^4.4.0" }, "funding": { @@ -4980,7 +4980,7 @@ }, "node_modules/typescript": { "version": "5.9.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "bin": { @@ -5017,7 +5017,7 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true }, @@ -5581,7 +5581,7 @@ }, "@kwsites/file-exists": { "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", "requires": { "debug": "^4.1.1" @@ -5589,7 +5589,7 @@ }, "@kwsites/promise-deferred": { "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" }, "@nodelib/fs.scandir": { @@ -5769,16 +5769,16 @@ "dev": true }, "@simple-git/args-pathspec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", - "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.4.tgz", + "integrity": "sha512-EtMX6XjRWSastG2SdkmSQByPtJ9dx/NIjnHbpQPCYt62j4RmSx5rgLTGpw0YCjF5h191gZOmBfheOT23cRSFdw==" }, "@simple-git/argv-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", - "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-2.0.1.tgz", + "integrity": "sha512-M++IaVWrN+vYalilOHfwvT2IL3wYZtVEBQpmX6DLSOJLtxbZ+SUeIDE3IiPogzhPACLajwtxfa5rp+ZX1FczzQ==", "requires": { - "@simple-git/args-pathspec": "^1.0.3" + "@simple-git/args-pathspec": "^1.0.4" } }, "@sindresorhus/merge-streams": { @@ -5872,7 +5872,7 @@ }, "@types/node": { "version": "20.19.26", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.26.tgz", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.26.tgz", "integrity": "sha512-0l6cjgF0XnihUpndDhk+nyD3exio3iKaYROSgvh/qSevPXax3L8p5DBRFjbvalnwatGgHEQn2R88y2fA3g4irg==", "dev": true, "requires": { @@ -5893,7 +5893,7 @@ }, "@types/vscode": { "version": "1.106.1", - "resolved": "https://registry.npmmirror.com/@types/vscode/-/vscode-1.106.1.tgz", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.106.1.tgz", "integrity": "sha512-R/HV8u2h8CAddSbX8cjpdd7B8/GnE4UjgjpuGuHcbp1xV6yh4OeqU4L1pKjlwujCrSFS0MOpwJAIs/NexMB1fQ==", "dev": true }, @@ -6478,7 +6478,7 @@ }, "debug": { "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { "ms": "^2.1.3" @@ -7601,7 +7601,7 @@ }, "ms": { "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "mute-stream": { @@ -8183,14 +8183,14 @@ } }, "simple-git": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", - "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-4.0.2.tgz", + "integrity": "sha512-l0sIsv9VrPwovKELFDBOj/lhmIcbMvxBk2B+hRhsMM4/VL3gKtuZvYnBNixVo+N/0HnqQx2cBdLvxnAbygHpqA==", "requires": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", - "@simple-git/args-pathspec": "^1.0.3", - "@simple-git/argv-parser": "^1.1.0", + "@simple-git/args-pathspec": "1.0.4", + "@simple-git/argv-parser": "2.0.1", "debug": "^4.4.0" } }, @@ -8506,7 +8506,7 @@ }, "typescript": { "version": "5.9.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true }, @@ -8530,7 +8530,7 @@ }, "undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true }, diff --git a/package.json b/package.json index 92121d8..925c4d9 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "watch": "tsc -watch -p ./", "lint": "eslint src", "package": "vsce package", + "check:lockfile": "node scripts/check-lockfile-registry.test.js && node scripts/check-lockfile-registry.js", "smoke:paths": "node scripts/smoke-paths.js", "smoke:status": "node scripts/smoke-status.js", "smoke:picker": "node scripts/smoke-picker.js", @@ -174,6 +175,6 @@ "typescript": "^5.x" }, "dependencies": { - "simple-git": "^3.22.0" + "simple-git": "^4.0.2" } } diff --git a/scripts/check-lockfile-registry.js b/scripts/check-lockfile-registry.js new file mode 100644 index 0000000..e0d1cec --- /dev/null +++ b/scripts/check-lockfile-registry.js @@ -0,0 +1,68 @@ +'use strict'; +// Fails when package-lock.json resolves any package from a registry other than +// the public npm registry. Run before `npm ci` so a mirror URL never reaches CI. +// Reads both lockfile layouts: `packages` (v2, v3) and the legacy +// `dependencies` tree (v1, and v2 for older npm). +const fs = require('fs'); +const path = require('path'); + +const ALLOWED_PREFIX = 'https://registry.npmjs.org/'; + +function isObject(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function checkEntry(label, entry, offenders) { + if (isObject(entry) && typeof entry.resolved === 'string' && !entry.resolved.startsWith(ALLOWED_PREFIX)) { + offenders.push(`${label}: ${entry.resolved}`); + } +} + +function walkLegacy(tree, trail, offenders) { + for (const [name, entry] of Object.entries(tree)) { + const label = `${trail}${name}`; + checkEntry(label, entry, offenders); + if (isObject(entry) && isObject(entry.dependencies)) { + walkLegacy(entry.dependencies, `${label} > `, offenders); + } + } +} + +// Returns { offenders }. Throws if the lockfile has neither layout. +function scanLockfile(lock) { + const hasPackages = isObject(lock) && isObject(lock.packages); + const hasLegacy = isObject(lock) && isObject(lock.dependencies); + if (!hasPackages && !hasLegacy) { + throw new Error('lockfile has neither a "packages" map nor a "dependencies" tree'); + } + const offenders = []; + if (hasPackages) { + for (const [name, entry] of Object.entries(lock.packages)) { + checkEntry(name || '(root)', entry, offenders); + } + } + if (hasLegacy) { + walkLegacy(lock.dependencies, '', offenders); + } + return { offenders }; +} + +module.exports = { scanLockfile, ALLOWED_PREFIX }; + +if (require.main === module) { + const lockPath = path.join(__dirname, '..', 'package-lock.json'); + let result; + try { + result = scanLockfile(JSON.parse(fs.readFileSync(lockPath, 'utf8'))); + } catch (err) { + console.error(`check-lockfile-registry: cannot read ${lockPath}: ${err.message}`); + process.exit(1); + } + if (result.offenders.length > 0) { + console.error(`package-lock.json resolves ${result.offenders.length} package(s) outside ${ALLOWED_PREFIX}:`); + for (const line of result.offenders) console.error(` ${line}`); + console.error(`Fix: run "npm install --package-lock-only --registry=${ALLOWED_PREFIX}" and commit the lockfile.`); + process.exit(1); + } + console.log('check-lockfile-registry OK'); +} diff --git a/scripts/check-lockfile-registry.test.js b/scripts/check-lockfile-registry.test.js new file mode 100644 index 0000000..e6d4f94 --- /dev/null +++ b/scripts/check-lockfile-registry.test.js @@ -0,0 +1,25 @@ +'use strict'; +// Self-test for check-lockfile-registry.js with one v1 and one v3 fixture. +const assert = require('assert'); +const { scanLockfile } = require('./check-lockfile-registry'); + +const npmjs = 'https://registry.npmjs.org/a/-/a-1.0.0.tgz'; +const mirror = 'https://registry.example.invalid/b/-/b-1.0.0.tgz'; + +// v1: only the legacy `dependencies` tree; the offender is nested. +const v1Clean = { lockfileVersion: 1, dependencies: { a: { resolved: npmjs, dependencies: { b: { resolved: npmjs } } } } }; +const v1Dirty = { lockfileVersion: 1, dependencies: { a: { resolved: npmjs, dependencies: { b: { resolved: mirror } } } } }; +assert.deepStrictEqual(scanLockfile(v1Clean).offenders, []); +assert.deepStrictEqual(scanLockfile(v1Dirty).offenders, [`a > b: ${mirror}`]); + +// v3: only the `packages` map. +const v3Clean = { lockfileVersion: 3, packages: { '': { name: 'x' }, 'node_modules/a': { resolved: npmjs } } }; +const v3Dirty = { lockfileVersion: 3, packages: { '': { name: 'x' }, 'node_modules/b': { resolved: mirror } } }; +assert.deepStrictEqual(scanLockfile(v3Clean).offenders, []); +assert.deepStrictEqual(scanLockfile(v3Dirty).offenders, [`node_modules/b: ${mirror}`]); + +// Neither layout: the check must fail, not pass on nothing. +assert.throws(() => scanLockfile({ lockfileVersion: 3 }), /neither/); +assert.throws(() => scanLockfile(null), /neither/); + +console.log('check-lockfile-registry self-test OK'); diff --git a/src/services/gitService.ts b/src/services/gitService.ts index 0a86576..13fcc35 100644 --- a/src/services/gitService.ts +++ b/src/services/gitService.ts @@ -1,4 +1,4 @@ -import simpleGit, { SimpleGit } from 'simple-git'; +import { simpleGit, SimpleGit } from 'simple-git'; import * as path from 'path'; import * as fs from 'fs'; import { STATUS_MAP, type FileStatus } from './fileStatus';