fix: write registry allowScripts keys under install-strategy=linked - #9941
fix: write registry allowScripts keys under install-strategy=linked#9941manzoorwanijk wants to merge 4 commits into
Conversation
fc5be25 to
4b40b17
Compare
|
@reggi this probably needs a label for v11 backport. |
|
Thanks for the quick fix! However it doesn't add the version number in // expected:
"allowScripts": {
"esbuild@0.28.1": true
}
// actual
"allowScripts": {
"esbuild": true
}I installed the fix locally using a local copy of the repo with this branch: ➜ npm -v
11.19.0
➜ node npm/bin/npm-cli.js -v
12.0.2Then, both of these commands gave me the output above: ➜ node npm/bin/npm-cli.js install-script approve esbuild@0.28.1
➜ node npm/bin/npm-cli.js install-script approve esbuild |
It works perfectly fine Screen.Recording.2026-09-01.at.3.15.02.PM.mov
|
Let's chalk it up to me not setting up the npm version from this PR properly |
|
I see that #9940 has a fix solely for the deduping (same fix as you have in |
Yes, it covers many other cases as well. |
This comment was marked as spam.
This comment was marked as spam.
|
@manzoorwanijk Thanks for addressing the comments, just one more thing I found: Preserve active versioned denies during pruningWith a missing or stale hidden lockfile, a linked package can have a trusted name but no trusted version. Given: {
"allowScripts": {
"canvas": true,
"canvas@1.0.0": false
}
}Runtime enforcement correctly blocks the package because the versioned deny cannot safely be ruled out. However, The missing argument in the prune caller predates this PR, but the new store-name fallback exposes this combination: the broad allow now survives pruning while its deny exception is discarded. Please pass deny intent to the matcher in - const matching = nodes.filter(({ node }) => matches(node, key))
+ const matching = nodes.filter(({ node }) => matches(node, key, value === false))The third argument is Regression coverageThe following can be added to for (const state of ['missing', 'stale']) {
t.test(`prune keeps denies with a ${state} hidden lockfile`, async t => {
const Arborist = require('@npmcli/arborist')
const isScriptAllowed = require('@npmcli/arborist/lib/script-allowed.js')
const allowScripts = {
canvas: true,
'canvas@1.0.0': false,
}
const { npm, prefix } = await mockNpm(t, {
prefixDir: setupLinkedProject(t, { allowScripts }),
config: { 'install-strategy': 'linked' },
})
const hidden = resolve(prefix, 'node_modules', '.package-lock.json')
if (state === 'missing') {
fs.unlinkSync(hidden)
} else {
fs.utimesSync(hidden, new Date(0), new Date(0))
}
t.equal(npm.config.get('omit-lockfile-registry-resolved'), false)
const arb = new Arborist({ ...npm.flatOptions, path: prefix })
const tree = await arb.loadActual()
const target = [...tree.inventory.values()]
.find(node => !node.isLink && node.name === 'canvas')
t.ok(target, 'finds the installed target')
t.strictSame(
isScriptAllowed.getTrustedRegistryIdentity(target),
{ name: 'canvas', version: null },
'recovers the registry name without inventing a trusted version'
)
t.equal(
isScriptAllowed(target, allowScripts),
false,
'the versioned deny blocks the package before pruning'
)
await npm.exec('install-scripts', ['prune'])
const pkg = JSON.parse(
fs.readFileSync(resolve(prefix, 'package.json'), 'utf8')
)
t.strictSame(
pkg.allowScripts,
allowScripts,
'pruning preserves both the allow and its active deny exception'
)
t.equal(
isScriptAllowed(target, pkg.allowScripts),
false,
'the package remains denied after pruning'
)
})
} |
|
@martinrrm thank you for the review.
Good find, fixed in c84ccaf. |
Under
install-strategy=linked,npm install-scripts approve <pkg>wrote verbose, duplicatedfile:entries pointing intonode_modules/.store(one per incoming symlink depth) instead ofname@versionpins, and those store-path entries never matched at install time.There are two root causes.
In
findNodesForArgs(allow-scripts-cmd.js), positional args matched everyLinkpointing at the store package; each Link's relativefile:.store/...resolved spec became its own policy key and could even strip the correct pin as stale.In
script-allowed.js, a store package has noedgesIn(they land on its incoming Links), soisRegistryNoderefused registry keys, andls, the post-install advisory, and prune treated a correctname@versionentry as matching nothing.The fix skips
Linknodes when matching positional args, mirroringcollectUnreviewedScriptsand prune, so approvals key off the real package's trusted registry identity.isRegistryNodeandnameFromEdgesnow delegate edge-based checks to a link target's incoming Links, which also coversomit-lockfile-registry-resolved(approve by name, like the hoisted #9558 path).resolvedSourceSpecsno longer fabricatesfile:specs from links into the store, so store packages are never keyed by store paths and prune cleans up the buggy entries while keeping the valid pin.References
Fixes #9939