From 78fd676b3872013865bbc25c46aa339dda0bf78f Mon Sep 17 00:00:00 2001 From: ychampion Date: Sun, 12 Jul 2026 15:32:57 +0000 Subject: [PATCH] fix: exempt explicit pack targets from allow-directory Constraint: allow-directory governs directory dependencies, not the package explicitly selected for pack or publish. Rejected: Override only in publish | leaves libnpmpack callers and npm pack inconsistent at the same policy boundary. Confidence: high Scope-risk: narrow Directive: Keep registry, remote, file, git, and dependency resolution subject to their configured fetch policies. Tested: node . run test; node . run test -w libnpmpack; publish and pack dry-run smoke for allow-directory=none|root; registry/file denial probes; git diff --check Not-tested: Manual CLI smoke on non-Linux platforms --- lib/commands/pack.js | 1 + test/lib/commands/pack.js | 19 +++++++++++++++++++ test/lib/commands/publish.js | 19 +++++++++++++++++++ workspaces/libnpmpack/lib/index.js | 5 +++++ workspaces/libnpmpack/test/index.js | 14 ++++++++++++++ 5 files changed, 58 insertions(+) diff --git a/lib/commands/pack.js b/lib/commands/pack.js index 8a4d2f0f74382..cce72d0389164 100644 --- a/lib/commands/pack.js +++ b/lib/commands/pack.js @@ -37,6 +37,7 @@ class Pack extends BaseCommand { const spec = npa(arg) const manifest = await pacote.manifest(spec, { ...this.npm.flatOptions, + ...(spec.type === 'directory' && { allowDirectory: 'all' }), Arborist, preferOnline: true, _isRoot: true, diff --git a/test/lib/commands/pack.js b/test/lib/commands/pack.js index 121992bdfc887..e880538fc7a69 100644 --- a/test/lib/commands/pack.js +++ b/test/lib/commands/pack.js @@ -111,6 +111,25 @@ t.test('dry run', async t => { t.throws(() => fs.statSync(path.resolve(npm.prefix, filename))) }) +for (const allowDirectory of ['none', 'root']) { + t.test(`dry run with allow-directory=${allowDirectory}`, async t => { + const { npm, outputs } = await loadMockNpm(t, { + prefixDir: { + 'package.json': JSON.stringify({ + name: 'test-package', + version: '1.0.0', + }), + }, + config: { + 'allow-directory': allowDirectory, + 'dry-run': true, + }, + }) + await npm.exec('pack', []) + t.strictSame(outputs, ['test-package-1.0.0.tgz']) + }) +} + t.test('foreground-scripts defaults to true', async t => { const { npm, outputs, logs } = await loadMockNpm(t, { prefixDir: { diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index fe286ff46b748..ec2c2fd9c8b66 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -151,6 +151,25 @@ t.test('dry-run', async t => { t.matchSnapshot(logs.notice) }) +for (const allowDirectory of ['none', 'root']) { + t.test(`dry-run with allow-directory=${allowDirectory}`, async t => { + const { joinedOutput, npm, registry } = await loadNpmWithRegistry(t, { + config: { + 'allow-directory': allowDirectory, + 'dry-run': true, + ...auth, + }, + prefixDir: { + 'package.json': JSON.stringify(pkgJson, null, 2), + }, + authorization: token, + }) + registry.publish(pkg, { noPut: true }) + await npm.exec('publish', []) + t.equal(joinedOutput(), `+ ${pkg}@1.0.0`) + }) +} + t.test('foreground-scripts defaults to true', async t => { const { outputs, npm, logs, registry } = await loadNpmWithRegistry(t, { config: { diff --git a/workspaces/libnpmpack/lib/index.js b/workspaces/libnpmpack/lib/index.js index 2ef6391e439d7..8bf68e2a400e9 100644 --- a/workspaces/libnpmpack/lib/index.js +++ b/workspaces/libnpmpack/lib/index.js @@ -12,6 +12,11 @@ async function pack (spec = 'file:.', opts = {}) { // gets spec spec = npa(spec) + // An explicit directory is the package being packed, not a dependency fetch. + if (spec.type === 'directory') { + opts = { ...opts, allowDirectory: 'all' } + } + const manifest = await pacote.manifest(spec, { ...opts, Arborist, _isRoot: true }) if (spec.type === 'directory') { diff --git a/workspaces/libnpmpack/test/index.js b/workspaces/libnpmpack/test/index.js index 1dc07ac2295a7..c6518b09b6570 100644 --- a/workspaces/libnpmpack/test/index.js +++ b/workspaces/libnpmpack/test/index.js @@ -36,6 +36,20 @@ t.test('packs from local directory', async t => { }) }) +for (const allowDirectory of ['none', 'root']) { + t.test(`packs an explicit local directory with allow-directory=${allowDirectory}`, async t => { + const testDir = t.testdir({ + 'package.json': JSON.stringify({ + name: 'my-cool-pkg', + version: '1.0.0', + }, null, 2), + }) + + const tarball = await pack(testDir, { allowDirectory }) + t.ok(tarball) + }) +} + t.test('flattens path separators in name so tarball stays in packDestination', async t => { const testDir = t.testdir({ src: {