From e1768e44cebba8c3ca41a0e589e8a70af7c2f6f8 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Mon, 10 Aug 2026 22:23:32 -0400 Subject: [PATCH] chore(deps): check the declared lockfileVersion, not just the packages key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From Copilot's fourteenth pass of #1962. `hasReadableLockShape` and the diagnostic it drives both promise "lockfileVersion 2+", but the check only looked for a `packages` table with a root entry. A file declaring v1 while carrying such a table was accepted, produced an empty version map, and could make a real skew read as aligned once only one other holder remained — the same fail-open the shape check was added to close, through the half of the contract that wasn't enforced. The declared version is now verified: a finite number, 2 or greater. Absent, non-numeric, and declared-v1-with-packages cases are all in the table. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3 --- scripts/verify-dep-lockstep.mjs | 9 ++++++++- scripts/verify-dep-lockstep.test.mjs | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/scripts/verify-dep-lockstep.mjs b/scripts/verify-dep-lockstep.mjs index 22ebb9ad..55553375 100644 --- a/scripts/verify-dep-lockstep.mjs +++ b/scripts/verify-dep-lockstep.mjs @@ -249,7 +249,14 @@ export function topLevelLockVersions(lock) { * cannot read it, so it must say so rather than skip the install. */ export function hasReadableLockShape(lock) { - const packages = lock?.packages; + // The declared version is checked, not just inferred from the presence of a + // `packages` key: this function and the diagnostic it drives both promise + // "lockfileVersion 2+", so a file claiming v1 while carrying a `packages` + // table must be rejected rather than half-trusted (Copilot, #1962). + const version = lock?.lockfileVersion; + if (typeof version !== "number" || !Number.isFinite(version) || version < 2) + return false; + const packages = lock.packages; if (typeof packages !== "object" || packages === null) return false; return Object.prototype.hasOwnProperty.call(packages, ""); } diff --git a/scripts/verify-dep-lockstep.test.mjs b/scripts/verify-dep-lockstep.test.mjs index a00ce642..55ca3b6f 100644 --- a/scripts/verify-dep-lockstep.test.mjs +++ b/scripts/verify-dep-lockstep.test.mjs @@ -302,11 +302,20 @@ test("hasReadableLockShape: only a v2+ packages table with a root entry (Copilot undefined, null, {}, - { packages: null }, - { packages: [] }, // an array has no `""` key - { packages: {} }, // no root entry - { packages: { "node_modules/zod": { version: "4.4.3" } } }, + { lockfileVersion: 3, packages: null }, + { lockfileVersion: 3, packages: [] }, // an array has no `""` key + { lockfileVersion: 3, packages: {} }, // no root entry + { + lockfileVersion: 3, + packages: { "node_modules/zod": { version: "4.4.3" } }, + }, { lockfileVersion: 1, dependencies: { zod: { version: "4.4.3" } } }, + // A declared v1 carrying a `packages` table: the version is checked, not + // inferred from the key's presence, so this is rejected rather than + // half-trusted into an empty (fail-open) version map. + { lockfileVersion: 1, packages: { "": {} } }, + { packages: { "": {} } }, // no declared version at all + { lockfileVersion: "3", packages: { "": {} } }, // not a number ]; for (const lock of rejected) assert.equal(hasReadableLockShape(lock), false, JSON.stringify(lock));