Skip to content

fix: write registry allowScripts keys under install-strategy=linked - #9941

Open
manzoorwanijk wants to merge 4 commits into
npm:latestfrom
manzoorwanijk:fix/linked-allow-scripts-store-keys
Open

fix: write registry allowScripts keys under install-strategy=linked#9941
manzoorwanijk wants to merge 4 commits into
npm:latestfrom
manzoorwanijk:fix/linked-allow-scripts-store-keys

Conversation

@manzoorwanijk

Copy link
Copy Markdown
Contributor

Under install-strategy=linked, npm install-scripts approve <pkg> wrote verbose, duplicated file: entries pointing into node_modules/.store (one per incoming symlink depth) instead of name@version pins, and those store-path entries never matched at install time.

There are two root causes.
In findNodesForArgs (allow-scripts-cmd.js), positional args matched every Link pointing at the store package; each Link's relative file:.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 no edgesIn (they land on its incoming Links), so isRegistryNode refused registry keys, and ls, the post-install advisory, and prune treated a correct name@version entry as matching nothing.

The fix skips Link nodes when matching positional args, mirroring collectUnreviewedScripts and prune, so approvals key off the real package's trusted registry identity.
isRegistryNode and nameFromEdges now delegate edge-based checks to a link target's incoming Links, which also covers omit-lockfile-registry-resolved (approve by name, like the hoisted #9558 path).
resolvedSourceSpecs no longer fabricates file: 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

@manzoorwanijk
manzoorwanijk force-pushed the fix/linked-allow-scripts-store-keys branch from fc5be25 to 4b40b17 Compare September 1, 2026 09:56
@manzoorwanijk
manzoorwanijk marked this pull request as ready for review September 1, 2026 10:07
@manzoorwanijk
manzoorwanijk requested review from a team as code owners September 1, 2026 10:07
@manzoorwanijk

Copy link
Copy Markdown
Contributor Author

@reggi this probably needs a label for v11 backport.

@nikolawork

Copy link
Copy Markdown

Thanks for the quick fix! However it doesn't add the version number in allowScripts:

// 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.2

Then, 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

@manzoorwanijk

Copy link
Copy Markdown
Contributor Author

it doesn't add the version number in allowScripts:

It works perfectly fine

Screen.Recording.2026-09-01.at.3.15.02.PM.mov

npm-dev is an alias that I have created for local clone.

@nikolawork

Copy link
Copy Markdown

However it doesn't add the version number in allowScripts

Let's chalk it up to me not setting up the npm version from this PR properly

@nikolawork

Copy link
Copy Markdown

I see that #9940 has a fix solely for the deduping (same fix as you have in ‎lib/utils/allow-scripts-cmd.js) as well as some tests for that specific use case. Do we test for deduping in the current PR as well?

@manzoorwanijk

Copy link
Copy Markdown
Contributor Author

I see that #9940 has a fix solely for the deduping (same fix as you have in ‎lib/utils/allow-scripts-cmd.js) as well as some tests for that specific use case. Do we test for deduping in the current PR as well?

Yes, it covers many other cases as well.

@martinrrm martinrrm self-assigned this Sep 3, 2026
Comment thread lib/utils/allow-scripts-cmd.js Outdated
Comment thread workspaces/arborist/lib/script-allowed.js Outdated
Comment thread workspaces/arborist/lib/script-allowed.js Outdated
Comment thread workspaces/arborist/lib/script-allowed.js
@twitschvimeo-gif

This comment was marked as spam.

@martinrrm

Copy link
Copy Markdown
Contributor

@manzoorwanijk Thanks for addressing the comments, just one more thing I found:

Preserve active versioned denies during pruning

With 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, npm install-scripts prune currently removes "canvas@1.0.0": false while retaining "canvas": true, changing the effective policy from denied to allowed.

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 lib/utils/allow-scripts-prune.js:

- const matching = nodes.filter(({ node }) => matches(node, key))
+ const matching = nodes.filter(({ node }) => matches(node, key, value === false))

The third argument is failClosed: a deny passes true, so an unverifiable version does not cause an active deny to be classified as unused. Known versions still use normal version matching.

Regression coverage

The following can be added to test/lib/commands/approve-scripts.js, after the existing setupLinkedProject helper. It reuses the existing fixture without removing registry URLs from the root lockfile or enabling omit-lockfile-registry-resolved.

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'
    )
  })
}

@manzoorwanijk

Copy link
Copy Markdown
Contributor Author

@martinrrm thank you for the review.

Preserve active versioned denies during pruning

With a missing or stale hidden lockfile, a linked package can have a trusted name but no trusted version.

Good find, fixed in c84ccaf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Verbose and duplicate entries in allowScripts created when install-strategy=linked

4 participants