From 1273dcad6f6a06419286529b9bcfb95daebfe861 Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Mon, 8 Jun 2026 22:30:03 +0530 Subject: [PATCH] fix(arborist): honor allow-remote=root for root-direct remote tarballs in linked strategy --- .../arborist/lib/arborist/isolated-reifier.js | 5 ++++ workspaces/arborist/lib/arborist/reify.js | 3 +- workspaces/arborist/lib/isolated-classes.js | 4 +++ workspaces/arborist/test/arborist/reify.js | 30 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/workspaces/arborist/lib/arborist/isolated-reifier.js b/workspaces/arborist/lib/arborist/isolated-reifier.js index e7d985a80a7b9..80c907aa49b6d 100644 --- a/workspaces/arborist/lib/arborist/isolated-reifier.js +++ b/workspaces/arborist/lib/arborist/isolated-reifier.js @@ -43,6 +43,7 @@ module.exports = cls => class IsolatedReifier extends cls { isInStore, inBundle, isRegistryDependency: node.isRegistryDependency, + isRootDependency: node.isRootDependency, location, name: node.packageName || node.name, optional: node.optional, @@ -157,6 +158,10 @@ module.exports = cls => class IsolatedReifier extends cls { // Carry the source node's registry-dependency flag so the store node retains it. // IsolatedNode has no edges to recompute it from, and reify's registry-tarball allow-remote exemption depends on it. result.isRegistryDependency = node.isRegistryDependency + // Same reasoning for allow-remote=root: the store node has no edgesIn, so capture from the source node whether it satisfies a valid edge from the project root or a workspace. + result.isRootDependency = [...node.edgesIn].some(e => + e.valid && (e.from?.isProjectRoot || e.from?.isWorkspace) + ) return result } diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index a3d80012e20f0..e76a277ebbf92 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -707,7 +707,8 @@ module.exports = cls => class Reifier extends cls { integrity: node.integrity, // A node counts as "root" for allow-* enforcement if it satisfies at least one valid dependency edge declared by the project root or a workspace. // node.parent is unsafe here: after hoisting, transitive packages can have the project root as their tree parent. - _isRoot: [...node.edgesIn].some(e => + // In the linked strategy the store node has no edgesIn, so isolated-reifier precomputes isRootDependency from the source node's edges. + _isRoot: node.isRootDependency || [...node.edgesIn].some(e => e.valid && (e.from?.isProjectRoot || e.from?.isWorkspace) ), // pacote's npa re-parses our `name@URL` spec as type=remote, so allowRemote would mis-fire on registry tarballs. diff --git a/workspaces/arborist/lib/isolated-classes.js b/workspaces/arborist/lib/isolated-classes.js index 007f2609e5feb..c4894e3da4437 100644 --- a/workspaces/arborist/lib/isolated-classes.js +++ b/workspaces/arborist/lib/isolated-classes.js @@ -21,6 +21,7 @@ class IsolatedNode { isInStore = false inBundle = false isRegistryDependency = false + isRootDependency = false linksIn = new Set() meta = { loadedFromDisk: false } optional = false @@ -54,6 +55,9 @@ class IsolatedNode { if (options.isRegistryDependency) { this.isRegistryDependency = true } + if (options.isRootDependency) { + this.isRootDependency = true + } if (options.optional) { this.optional = true } diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index 2abe5a5ed09a2..370124e3e645b 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -3904,6 +3904,36 @@ t.test('should preserve exact ranges, missing actual tree', async (t) => { await t.resolves(arb.reify(), 'registry tarball is allowed under linked strategy') }) + t.test('allowRemote=root allows root-direct remote tarball under linked install strategy', async t => { + // The linked strategy extracts store nodes as IsolatedNode, which has no edgesIn to recompute root-ness from. + // isRootDependency must be carried from the source tree node, otherwise allow-remote=root mis-fires on a genuine remote tarball that is a direct dep of the project root. + const testdir = t.testdir({ + project: { + 'package.json': JSON.stringify({ + name: 'myproject', + version: '1.0.0', + dependencies: { + abbrev: 'https://remote.example.com/abbrev-1.1.1.tgz', + }, + }), + }, + }) + + tnock(t, 'https://remote.example.com') + .get('/abbrev-1.1.1.tgz') + .reply(200, abbrevTGZ) + + const arb = new Arborist({ + path: resolve(testdir, 'project'), + registry: 'https://registry.example.com', + cache: resolve(testdir, 'cache'), + allowRemote: 'root', + installStrategy: 'linked', + }) + + await t.resolves(arb.reify(), 'root-direct remote tarball is allowed under linked strategy with allow-remote=root') + }) + t.test('registry with different protocol should swap protocol', async (t) => { const abbrevPackument4 = JSON.stringify({ _id: 'abbrev',