diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index c49f352cef140..15d499e7fb5d9 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -888,12 +888,14 @@ module.exports = cls => class Reifier extends cls { return false } try { - const resolved = new URL(node.resolved) + // Match the effective fetch URL, not the raw lockfile value. + // #registryResolved applies replace-registry-host, rewriting a public-registry pin to the configured proxy/mirror so it matches. + const resolvedURL = new URL(this.#registryResolved(node.resolved)) // pickRegistry only consults spec.scope, so a bare-name (tag) parse is sufficient and avoids a node.version dependency. const registry = new URL(pickRegistry(npa(node.name), this.options)) const registryPath = registry.pathname.replace(/\/?$/, '/') - return resolved.origin === registry.origin && - (registryPath === '/' || resolved.pathname.startsWith(registryPath)) + return resolvedURL.origin === registry.origin && + (registryPath === '/' || resolvedURL.pathname.startsWith(registryPath)) } catch { return false } diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index d80d4d2998758..87144c3efb474 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -4027,6 +4027,106 @@ t.test('should preserve exact ranges, missing actual tree', async (t) => { await t.resolves(arb.reify(), 'same-origin tarball is allowed for registry root') }) + t.test('allowRemote=none allows registry tarball whose resolved origin differs from the configured registry', async t => { + // Proxy/mirror case: a committed lockfile pins resolved to the public registry while a private mirror is configured. + // replace-registry-host rewrites the host to the configured registry at fetch time, so the effective URL is registry-mediated and must pass allow-remote=none. + const abbrevPackumentNpmjs = JSON.stringify({ + _id: 'abbrev', + _rev: 'lkjadflkjasdf', + name: 'abbrev', + 'dist-tags': { latest: '1.1.1' }, + versions: { + '1.1.1': { + name: 'abbrev', + version: '1.1.1', + dist: { + tarball: 'https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz', + }, + }, + }, + }) + + const testdir = t.testdir({ + project: { + 'package.json': JSON.stringify({ + name: 'myproject', + version: '1.0.0', + dependencies: { + abbrev: '1.1.1', + }, + }), + }, + }) + + tnock(t, 'https://registry.example.com') + .get('/abbrev') + .reply(200, abbrevPackumentNpmjs) + + // replace-registry-host (default 'npmjs') rewrites the npmjs.org tarball host to the configured mirror, so the fetch lands here. + tnock(t, 'https://registry.example.com') + .get('/abbrev/-/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: 'none', + }) + + await t.resolves(arb.reify(), 'mirror-fronted registry tarball is allowed under allow-remote=none') + }) + + t.test('allowRemote=none allows registry tarball with replaceRegistryHost=always', async t => { + // replace-registry-host=always routes every registry tarball fetch through the configured registry, so the effective URL is never remote and must pass allow-remote=none. + const abbrevPackumentNpmjs = JSON.stringify({ + _id: 'abbrev', + _rev: 'lkjadflkjasdf', + name: 'abbrev', + 'dist-tags': { latest: '1.1.1' }, + versions: { + '1.1.1': { + name: 'abbrev', + version: '1.1.1', + dist: { + tarball: 'https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz', + }, + }, + }, + }) + + const testdir = t.testdir({ + project: { + 'package.json': JSON.stringify({ + name: 'myproject', + version: '1.0.0', + dependencies: { + abbrev: '1.1.1', + }, + }), + }, + }) + + tnock(t, 'https://registry.example.com') + .get('/npm/abbrev') + .reply(200, abbrevPackumentNpmjs) + + // always rewrites the tarball host to the configured registry and prepends the registry path. + tnock(t, 'https://registry.example.com') + .get('/npm/abbrev/-/abbrev-1.1.1.tgz') + .reply(200, abbrevTGZ) + + const arb = new Arborist({ + path: resolve(testdir, 'project'), + registry: 'https://registry.example.com/npm', + cache: resolve(testdir, 'cache'), + allowRemote: 'none', + replaceRegistryHost: 'always', + }) + + await t.resolves(arb.reify(), 'registry tarball routed through the configured registry is allowed') + }) + t.test('allowRemote=none allows registry tarball under linked install strategy', async t => { // The linked strategy extracts store nodes as IsolatedNode, which has no edges to recompute isRegistryDependency from. // The flag must be carried from the source tree node so the registry-tarball allow-remote exemption still applies.