Skip to content

Commit fc9d4c7

Browse files
feat: namespace install-script approval commands under npm install-scripts (#9635)
Backport of #9629 to `release/v11`. Adds the namespaced `npm install-scripts` command (`approve`, `deny`, `ls`), keeping `npm approve-scripts` / `npm deny-scripts` working as aliases, and points the install-time, rebuild, and strict-allow-scripts guidance at the new namespace. ## References Backports #9629
1 parent 03cee43 commit fc9d4c7

18 files changed

Lines changed: 483 additions & 71 deletions
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
title: npm-install-scripts
3+
section: 1
4+
description: Manage install-script approvals for dependencies
5+
---
6+
7+
### Synopsis
8+
9+
<!-- AUTOGENERATED USAGE DESCRIPTIONS -->
10+
11+
### Description
12+
13+
Manages the `allowScripts` field in your project's `package.json`, which
14+
records which of your dependencies are permitted to run install scripts
15+
(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry
16+
sources). This is the recommended way to maintain that field.
17+
18+
Dependency install scripts are blocked by default. Install commands
19+
silently skip lifecycle scripts for any dependency that does not have a
20+
matching entry in `allowScripts`, and end with a list of the packages
21+
whose scripts were skipped so you can review them here.
22+
23+
This command only works inside a project that has a `package.json`. Running
24+
it with `--global` (`-g`) fails with an `EGLOBAL` error, since global
25+
installs (`npm install -g`) and one-off executions (`npm exec` / `npx`) have
26+
no project `package.json` to write to. To allow install scripts in those
27+
contexts, use the `--allow-scripts` flag at install time (for example
28+
`npm install -g --allow-scripts=canvas,sharp`) or persist the setting with
29+
`npm config set allow-scripts=canvas,sharp --location=user`.
30+
31+
There are three subcommands:
32+
33+
```bash
34+
npm install-scripts approve <pkg> [<pkg> ...]
35+
npm install-scripts approve --all
36+
npm install-scripts deny <pkg> [<pkg> ...]
37+
npm install-scripts deny --all
38+
npm install-scripts ls
39+
```
40+
41+
`approve` allows install scripts for the named packages. `<pkg>` matches
42+
every installed version of that package. By default it writes pinned entries
43+
(`[email protected]`), which keep their approval narrowed to the specific version you
44+
reviewed. Pass `--no-allow-scripts-pin` to write name-only entries that allow
45+
any future version. `--all` approves every package with unreviewed install
46+
scripts in one go.
47+
48+
`deny` records an explicit denial for the named packages (a name-only `false`
49+
entry), which survives `npm install-scripts approve --all` and excludes the
50+
package from any future blanket approval. `--all` denies every package with
51+
unreviewed install scripts.
52+
53+
`ls` is read-only: it lists every package whose install scripts are not yet
54+
covered by `allowScripts`, without modifying `package.json`.
55+
56+
`approve` honours the asymmetric pin rule: if you re-approve a package whose
57+
installed version has changed, the existing pin is rewritten to track the new
58+
installed version. Multi-version statements (`pkg@1 || 2`) are left alone,
59+
since they likely capture intent that the command cannot infer. Existing
60+
`false` entries always win; `approve` will not silently re-allow a package you
61+
previously denied.
62+
63+
The standalone commands [`npm approve-scripts`](/commands/npm-approve-scripts)
64+
and [`npm deny-scripts`](/commands/npm-deny-scripts) are aliases for
65+
`npm install-scripts approve` and `npm install-scripts deny`.
66+
67+
### Examples
68+
69+
```bash
70+
# Approve all currently-installed install scripts after reviewing them
71+
npm install-scripts approve --all
72+
73+
# Approve specific packages, pinned to their installed version
74+
npm install-scripts approve canvas sharp
75+
76+
# Deny a package so it stays blocked
77+
npm install-scripts deny telemetry-pkg
78+
79+
# Preview which packages still need review
80+
npm install-scripts ls
81+
```
82+
83+
### Configuration
84+
85+
<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->
86+
87+
### See Also
88+
89+
* [npm approve-scripts](/commands/npm-approve-scripts)
90+
* [npm deny-scripts](/commands/npm-deny-scripts)
91+
* [npm install](/commands/npm-install)
92+
* [npm rebuild](/commands/npm-rebuild)
93+
* [package.json](/configuring-npm/package-json)

docs/lib/content/nav.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@
9393
- title: npm install-ci-test
9494
url: /commands/npm-install-ci-test
9595
description: Install a project with a clean slate and run tests
96+
- title: npm install-scripts
97+
url: /commands/npm-install-scripts
98+
description: Manage install-script approvals for dependencies
9699
- title: npm install-test
97100
url: /commands/npm-install-test
98101
description: Install package(s) and run tests

lib/commands/install-scripts.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const AllowScriptsCmd = require('../utils/allow-scripts-cmd.js')
2+
3+
// Namespaced front-end for managing install-script approvals.
4+
// `approve` and `deny` write the `allowScripts` policy; `ls` lists packages with unreviewed install scripts.
5+
// The standalone `npm approve-scripts` and `npm deny-scripts` commands remain as aliases for `approve` and `deny`.
6+
class InstallScripts extends AllowScriptsCmd {
7+
static description = 'Manage install-script approvals for dependencies'
8+
static name = 'install-scripts'
9+
static usage = [
10+
'approve <pkg> [<pkg> ...]',
11+
'approve --all',
12+
'deny <pkg> [<pkg> ...]',
13+
'deny --all',
14+
'ls',
15+
]
16+
17+
static params = ['all', 'allow-scripts-pin', 'json']
18+
19+
static async completion (opts) {
20+
const argv = opts.conf.argv.remain
21+
const subcommands = ['approve', 'deny', 'ls']
22+
if (argv.length === 2) {
23+
return subcommands
24+
}
25+
if (subcommands.includes(argv[2])) {
26+
return []
27+
}
28+
throw new Error(`${argv[2]} not recognized`)
29+
}
30+
31+
async exec (args) {
32+
const [sub, ...rest] = args
33+
switch (sub) {
34+
case 'approve':
35+
return this.runMode('approve', rest)
36+
case 'deny':
37+
return this.runMode('deny', rest)
38+
case 'ls':
39+
case 'list':
40+
return this.runMode('list', rest)
41+
default:
42+
throw this.usageError(
43+
sub ? `\`${sub}\` is not a recognized subcommand.` : undefined
44+
)
45+
}
46+
}
47+
}
48+
49+
module.exports = InstallScripts

lib/commands/rebuild.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,13 @@ class Rebuild extends ArboristWorkspaceCmd {
7575
if (unreviewed.length > 0) {
7676
const count = unreviewed.length
7777
const noun = count === 1 ? 'package has' : 'packages have'
78-
// `npm approve-scripts` writes to a project package.json, which doesn't
78+
// `npm install-scripts` writes to a project package.json, which doesn't
7979
// exist for global rebuilds. Point global users at `npm config set`,
8080
// which writes the `allow-scripts` setting to their user .npmrc.
8181
const names = unreviewed.map(({ node }) => trustedDisplay(node).name)
8282
const remediation = this.npm.global
8383
? `Run \`${configSetAllowScripts(names)}\` to allow their scripts.`
84-
: 'Run `npm approve-scripts --allow-scripts-pending` to review.'
84+
: 'Run `npm install-scripts ls` to review.'
8585
log.warn(
8686
'rebuild',
8787
`${count} ${noun} install scripts not yet covered by allowScripts. ` +

lib/utils/allow-scripts-cmd.js

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ const parsePositional = (arg) => {
3434
return { name, range: null }
3535
}
3636

37-
// Shared implementation for `npm approve-scripts` and `npm deny-scripts`.
38-
// Subclasses set `verb` to `'approve'` or `'deny'`.
37+
// Shared implementation for `npm approve-scripts`, `npm deny-scripts`, and the `npm install-scripts` namespace.
38+
// `npm install-scripts` dispatches to `runMode('approve' | 'deny' | 'list', ...)`.
39+
// The standalone commands set `static verb` and run through the default `exec`.
3940
//
4041
// Extends `BaseCommand` rather than `ArboristCmd` on purpose. Per RFC,
4142
// `allowScripts` is read from the workspace root's `package.json` only;
@@ -48,33 +49,51 @@ class AllowScriptsCmd extends BaseCommand {
4849
static params = ['all', 'allow-scripts-pending', 'allow-scripts-pin', 'json']
4950
static ignoreImplicitWorkspace = false
5051

51-
// Subclasses set `static verb = 'approve' | 'deny'`.
52+
// Mode of the current run, set by runMode.
53+
// One of 'approve', 'deny', or 'list'.
54+
#mode = null
55+
56+
// verb drives the writers and summaries, which only run in the two write modes, so it is never read while listing.
5257
get verb () {
53-
/* istanbul ignore next: every concrete subclass declares static verb */
54-
return this.constructor.verb
58+
return this.#mode
5559
}
5660

61+
// Standalone `npm approve-scripts` / `npm deny-scripts` pick their mode from `static verb`.
5762
async exec (args) {
63+
return this.runMode(this.constructor.verb, args)
64+
}
65+
66+
async runMode (mode, args) {
67+
this.#mode = mode
68+
5869
if (this.npm.global) {
5970
throw Object.assign(
6071
new Error(`\`npm ${this.constructor.name}\` does not work for global installs`),
6172
{ code: 'EGLOBAL' }
6273
)
6374
}
6475

65-
const pending = !!this.npm.config.get('allow-scripts-pending')
76+
// `--allow-scripts-pending` is only honored by commands that declare it; the namespace lists via `ls` instead.
77+
const pending = this.constructor.params.includes('allow-scripts-pending') &&
78+
!!this.npm.config.get('allow-scripts-pending')
6679
const all = !!this.npm.config.get('all')
80+
// The `ls` subcommand lists, and so does `--allow-scripts-pending` on the write commands.
81+
const list = mode === 'list' || pending
6782

68-
if (pending && (args.length > 0 || all)) {
83+
if (list && (args.length > 0 || all)) {
84+
const what = mode === 'list' ? '`npm install-scripts ls`' : '`--allow-scripts-pending`'
6985
throw this.usageError(
70-
'`--allow-scripts-pending` cannot be combined with positional arguments or `--all`.'
86+
`${what} cannot be combined with positional arguments or \`--all\`.`
7187
)
7288
}
73-
if (!pending && !all && args.length === 0) {
89+
if (!list && !all && args.length === 0) {
7490
throw this.usageError()
7591
}
76-
if (this.verb === 'deny' && pending) {
77-
throw this.usageError('`npm deny-scripts --allow-scripts-pending` is not supported.')
92+
if (mode === 'deny' && pending) {
93+
throw this.usageError(
94+
'`npm deny-scripts --allow-scripts-pending` is not supported; ' +
95+
'run `npm install-scripts ls` to list unreviewed packages.'
96+
)
7897
}
7998

8099
const Arborist = require('@npmcli/arborist')
@@ -91,7 +110,7 @@ class AllowScriptsCmd extends BaseCommand {
91110
// only lists; nothing runs.
92111
const unreviewed = await checkAllowScripts({ arb, npm: this.npm, includeWhenIgnored: true })
93112

94-
if (pending) {
113+
if (list) {
95114
return this.runPending(unreviewed)
96115
}
97116

@@ -129,7 +148,8 @@ class AllowScriptsCmd extends BaseCommand {
129148
}
130149
output.standard('')
131150
output.standard(
132-
'Run `npm approve-scripts <pkg>` to allow, or `npm deny-scripts <pkg>` to deny.'
151+
'Run `npm install-scripts approve <pkg>` to allow, ' +
152+
'or `npm install-scripts deny <pkg>` to deny.'
133153
)
134154
}
135155

lib/utils/allow-scripts-writer.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,15 @@ const isSingleVersionPin = (key) => {
108108
// an approval. Per RFC, a name-only deny ("pkg": false) is widest and
109109
// the only remediation is to remove the entry. A versioned deny
110110
// ("[email protected]": false or a disjunction) blocks only specific versions;
111-
// the user can either widen it via `npm deny-scripts <name>` or remove
112-
// it to approve the currently-installed version only.
111+
// the user can either widen it via `npm install-scripts deny <name>` or
112+
// remove it to approve the currently-installed version only.
113113
const denyWarning = (key, subject, name) => {
114114
if (isNameOnlyKey(key)) {
115115
return `${key} is denied; remove the entry from allowScripts to approve ${subject}.`
116116
}
117117
/* istanbul ignore next: name fallback is defensive; callers pass nameKeyFor(sample) */
118118
const widenTarget = name || 'this package'
119-
return `${key} is a versioned deny; run \`npm deny-scripts ${widenTarget}\` ` +
119+
return `${key} is a versioned deny; run \`npm install-scripts deny ${widenTarget}\` ` +
120120
`to widen the deny to all versions of ${widenTarget}, or remove the entry ` +
121121
`to approve ${subject}.`
122122
}

lib/utils/cmd-list.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const commands = [
3131
'init',
3232
'install',
3333
'install-ci-test',
34+
'install-scripts',
3435
'install-test',
3536
'link',
3637
'll',

lib/utils/reify-output.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ const unreviewedScriptsMessage = (npm, unreviewedScripts) => {
268268
)
269269
}
270270

271-
// `npm approve-scripts` writes to a project package.json, which doesn't
271+
// `npm install-scripts` writes to a project package.json, which doesn't
272272
// exist for global installs (it throws EGLOBAL). For those, point users at
273273
// the mechanism that does work globally: the `--allow-scripts` flag for a
274274
// one-off, or `npm config set allow-scripts` to persist it.
@@ -282,8 +282,8 @@ const remediationLines = (npm, names) => {
282282
]
283283
}
284284
return [
285-
'Run `npm approve-scripts --allow-scripts-pending` to review, ' +
286-
'or `npm approve-scripts <pkg>` to allow.',
285+
'Run `npm install-scripts ls` to review, ' +
286+
'or `npm install-scripts approve <pkg>` to allow.',
287287
]
288288
}
289289

lib/utils/strict-allow-scripts-preflight.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,19 @@ const strictAllowScriptsPreflight = async ({ arb, npm, idealTreeOpts }) => {
4848
return ` ${label} (${events})`
4949
}).join('\n')
5050

51-
// `npm approve-scripts` / `npm deny-scripts` write to a project
52-
// package.json, which doesn't exist for global installs. Point global
53-
// users at the `--allow-scripts` flag and `npm config set allow-scripts`,
54-
// which both work for global installs. Use the trusted display identity
55-
// so the suggested `npm config set` value matches what the policy matches
56-
// on, not the tarball's self-reported name.
51+
// `npm install-scripts` writes to a project package.json, which doesn't
52+
// exist for global installs. Point global users at the `--allow-scripts`
53+
// flag and `npm config set allow-scripts`, which both work for global
54+
// installs. Use the trusted display identity so the suggested `npm config
55+
// set` value matches what the policy matches on, not the tarball's
56+
// self-reported name.
5757
const names = unreviewed.map(({ node }) => trustedDisplay(node).name)
5858
const remediation = npm.global
5959
? 'Allow them with `--allow-scripts`, persist them with ' +
6060
`\`${configSetAllowScripts(names)}\`, or bypass this ` +
6161
'check with `--dangerously-allow-all-scripts`.'
62-
: 'Approve them with `npm approve-scripts`, deny them with ' +
63-
'`npm deny-scripts`, or bypass this check with ' +
62+
: 'Approve them with `npm install-scripts approve`, deny them with ' +
63+
'`npm install-scripts deny`, or bypass this check with ' +
6464
'`--dangerously-allow-all-scripts`.'
6565

6666
throw Object.assign(

smoke-tests/tap-snapshots/test/index.js.test.cjs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ All commands:
2525
completion, config, dedupe, deny-scripts, deprecate, diff,
2626
dist-tag, docs, doctor, edit, exec, explain, explore,
2727
find-dupes, fund, get, help, help-search, init, install,
28-
install-ci-test, install-test, link, ll, login, logout, ls,
29-
org, outdated, owner, pack, ping, pkg, prefix, profile,
30-
prune, publish, query, rebuild, repo, restart, root, run,
31-
sbom, search, set, shrinkwrap, stage, star, stars, start,
32-
stop, team, test, token, trust, undeprecate, uninstall,
33-
unpublish, unstar, update, version, view, whoami
28+
install-ci-test, install-scripts, install-test, link, ll,
29+
login, logout, ls, org, outdated, owner, pack, ping, pkg,
30+
prefix, profile, prune, publish, query, rebuild, repo,
31+
restart, root, run, sbom, search, set, shrinkwrap, stage,
32+
star, stars, start, stop, team, test, token, trust,
33+
undeprecate, uninstall, unpublish, unstar, update, version,
34+
view, whoami
3435
3536
Specify configs in the ini-formatted file:
3637
{NPM}/{TESTDIR}/home/.npmrc

0 commit comments

Comments
 (0)