Skip to content

Commit 0a69db4

Browse files
authored
feat: refuse to set deprecated/invalid config (#5719)
BREAKING CHANGE: `npm config set` will no longer accept deprecated or invalid config options.
1 parent fc82298 commit 0a69db4

File tree

2 files changed

+130
-66
lines changed

2 files changed

+130
-66
lines changed

lib/commands/config.js

+23-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ const localeCompare = require('@isaacs/string-locale-compare')('en')
1010
const rpj = require('read-package-json-fast')
1111
const log = require('../utils/log-shim.js')
1212

13+
// These are the configs that we can nerf-dart. Not all of them currently even
14+
// *have* config definitions so we have to explicitly validate them here
15+
const nerfDarts = [
16+
'_auth',
17+
'_authToken',
18+
'username',
19+
'_password',
20+
'email',
21+
'certfile',
22+
'keyfile',
23+
]
24+
1325
// take an array of `[key, value, k2=v2, k3, v3, ...]` and turn into
1426
// { key: value, k2: v2, k3: v3 }
1527
const keyValues = args => {
@@ -146,6 +158,16 @@ class Config extends BaseCommand {
146158
const where = this.npm.flatOptions.location
147159
for (const [key, val] of Object.entries(keyValues(args))) {
148160
log.info('config', 'set %j %j', key, val)
161+
const baseKey = key.split(':').pop()
162+
if (!this.npm.config.definitions[baseKey] && !nerfDarts.includes(baseKey)) {
163+
throw new Error(`\`${baseKey}\` is not a valid npm option`)
164+
}
165+
const deprecated = this.npm.config.definitions[baseKey]?.deprecated
166+
if (deprecated) {
167+
throw new Error(
168+
`The \`${baseKey}\` option is deprecated, and can not be set in this way${deprecated}`
169+
)
170+
}
149171
this.npm.config.set(key, val || '', where)
150172
if (!this.npm.config.validate(where)) {
151173
log.warn('config', 'omitting invalid config values')
@@ -163,7 +185,7 @@ class Config extends BaseCommand {
163185
const out = []
164186
for (const key of keys) {
165187
if (!publicVar(key)) {
166-
throw new Error(`The ${key} option is protected, and cannot be retrieved in this way`)
188+
throw new Error(`The ${key} option is protected, and can not be retrieved in this way`)
167189
}
168190

169191
const pref = keys.length > 1 ? `${key}=` : ''

test/lib/commands/config.js

+107-65
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
const { join } = require('path')
2-
const { promisify } = require('util')
3-
const fs = require('fs')
2+
const fs = require('fs/promises')
3+
const ini = require('ini')
44
const tspawk = require('../../fixtures/tspawk')
55
const t = require('tap')
66

77
const spawk = tspawk(t)
88

9-
const readFile = promisify(fs.readFile)
10-
119
const Sandbox = require('../../fixtures/sandbox.js')
1210

1311
t.test('config no args', async t => {
@@ -142,60 +140,100 @@ t.test('config delete no args', async t => {
142140
t.test('config delete single key', async t => {
143141
// location defaults to user, so we work with a userconfig
144142
const home = t.testdir({
145-
'.npmrc': 'foo=bar\nbar=baz',
143+
'.npmrc': 'access=public\nall=true',
146144
})
147145

148146
const sandbox = new Sandbox(t)
149-
await sandbox.run('config', ['delete', 'foo'], { home })
147+
await sandbox.run('config', ['delete', 'access'], { home })
150148

151-
t.equal(sandbox.config.get('foo'), undefined, 'foo should no longer be set')
149+
t.equal(sandbox.config.get('access'), null, 'acces should be defaulted')
152150

153-
const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' })
154-
t.not(contents.includes('foo='), 'foo was removed on disk')
151+
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
152+
const rc = ini.parse(contents)
153+
t.not(rc.access, 'access is not set')
155154
})
156155

157156
t.test('config delete multiple keys', async t => {
158157
const home = t.testdir({
159-
'.npmrc': 'foo=bar\nbar=baz\nbaz=buz',
158+
'.npmrc': 'access=public\nall=true\naudit=false',
160159
})
161160

162161
const sandbox = new Sandbox(t)
163-
await sandbox.run('config', ['delete', 'foo', 'bar'], { home })
162+
await sandbox.run('config', ['delete', 'access', 'all'], { home })
164163

165-
t.equal(sandbox.config.get('foo'), undefined, 'foo should no longer be set')
166-
t.equal(sandbox.config.get('bar'), undefined, 'bar should no longer be set')
164+
t.equal(sandbox.config.get('access'), null, 'access should be defaulted')
165+
t.equal(sandbox.config.get('all'), false, 'all should be defaulted')
167166

168-
const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' })
169-
t.not(contents.includes('foo='), 'foo was removed on disk')
170-
t.not(contents.includes('bar='), 'bar was removed on disk')
167+
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
168+
const rc = ini.parse(contents)
169+
t.not(rc.access, 'access is not set')
170+
t.not(rc.all, 'all is not set')
171171
})
172172

173173
t.test('config delete key --location=global', async t => {
174174
const global = t.testdir({
175-
npmrc: 'foo=bar\nbar=baz',
175+
npmrc: 'access=public\nall=true',
176176
})
177177

178178
const sandbox = new Sandbox(t)
179-
await sandbox.run('config', ['delete', 'foo', '--location=global'], { global })
179+
await sandbox.run('config', ['delete', 'access', '--location=global'], { global })
180180

181-
t.equal(sandbox.config.get('foo', 'global'), undefined, 'foo should no longer be set')
181+
t.equal(sandbox.config.get('access', 'global'), undefined, 'access should be defaulted')
182182

183-
const contents = await readFile(join(global, 'npmrc'), { encoding: 'utf8' })
184-
t.not(contents.includes('foo='), 'foo was removed on disk')
183+
const contents = await fs.readFile(join(global, 'npmrc'), { encoding: 'utf8' })
184+
const rc = ini.parse(contents)
185+
t.not(rc.access, 'access is not set')
185186
})
186187

187188
t.test('config delete key --global', async t => {
188189
const global = t.testdir({
189-
npmrc: 'foo=bar\nbar=baz',
190+
npmrc: 'access=public\nall=true',
190191
})
191192

192193
const sandbox = new Sandbox(t)
193-
await sandbox.run('config', ['delete', 'foo', '--global'], { global })
194+
await sandbox.run('config', ['delete', 'access', '--global'], { global })
195+
196+
t.equal(sandbox.config.get('access', 'global'), undefined, 'access should no longer be set')
197+
198+
const contents = await fs.readFile(join(global, 'npmrc'), { encoding: 'utf8' })
199+
const rc = ini.parse(contents)
200+
t.not(rc.access, 'access is not set')
201+
})
202+
203+
t.test('config set invalid option', async t => {
204+
const sandbox = new Sandbox(t)
205+
await t.rejects(
206+
sandbox.run('config', ['set', 'nonexistantconfigoption', 'something']),
207+
/not a valid npm option/
208+
)
209+
})
210+
211+
t.test('config set deprecated option', async t => {
212+
const sandbox = new Sandbox(t)
213+
await t.rejects(
214+
sandbox.run('config', ['set', 'shrinkwrap', 'true']),
215+
/deprecated/
216+
)
217+
})
194218

195-
t.equal(sandbox.config.get('foo', 'global'), undefined, 'foo should no longer be set')
219+
t.test('config set nerf-darted option', async t => {
220+
const sandbox = new Sandbox(t)
221+
await sandbox.run('config', ['set', '//npm.pkg.github.com/:_authToken', '0xdeadbeef'])
222+
t.equal(
223+
sandbox.config.get('//npm.pkg.github.com/:_authToken'),
224+
'0xdeadbeef',
225+
'nerf-darted config is set'
226+
)
227+
})
196228

197-
const contents = await readFile(join(global, 'npmrc'), { encoding: 'utf8' })
198-
t.not(contents.includes('foo='), 'foo was removed on disk')
229+
t.test('config set scoped optoin', async t => {
230+
const sandbox = new Sandbox(t)
231+
await sandbox.run('config', ['set', '@npm:registry', 'https://registry.npmjs.org'])
232+
t.equal(
233+
sandbox.config.get('@npm:registry'),
234+
'https://registry.npmjs.org',
235+
'scoped config is set'
236+
)
199237
})
200238

201239
t.test('config set no args', async t => {
@@ -212,65 +250,67 @@ t.test('config set no args', async t => {
212250

213251
t.test('config set key', async t => {
214252
const home = t.testdir({
215-
'.npmrc': 'foo=bar',
253+
'.npmrc': 'access=public',
216254
})
217255

218256
const sandbox = new Sandbox(t, { home })
219257

220-
await sandbox.run('config', ['set', 'foo'])
258+
await sandbox.run('config', ['set', 'access'])
221259

222-
t.equal(sandbox.config.get('foo'), '', 'set the value for foo')
260+
t.equal(sandbox.config.get('access'), null, 'set the value for access')
223261

224-
const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' })
225-
t.ok(contents.includes('foo='), 'wrote foo to disk')
262+
await t.rejects(fs.stat(join(home, '.npmrc'), { encoding: 'utf8' }), 'removed empty config')
226263
})
227264

228265
t.test('config set key value', async t => {
229266
const home = t.testdir({
230-
'.npmrc': 'foo=bar',
267+
'.npmrc': 'access=public',
231268
})
232269

233270
const sandbox = new Sandbox(t, { home })
234271

235-
await sandbox.run('config', ['set', 'foo', 'baz'])
272+
await sandbox.run('config', ['set', 'access', 'restricted'])
236273

237-
t.equal(sandbox.config.get('foo'), 'baz', 'set the value for foo')
274+
t.equal(sandbox.config.get('access'), 'restricted', 'set the value for access')
238275

239-
const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' })
240-
t.ok(contents.includes('foo=baz'), 'wrote foo to disk')
276+
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
277+
const rc = ini.parse(contents)
278+
t.equal(rc.access, 'restricted', 'access is set to restricted')
241279
})
242280

243281
t.test('config set key=value', async t => {
244282
const home = t.testdir({
245-
'.npmrc': 'foo=bar',
283+
'.npmrc': 'access=public',
246284
})
247285

248286
const sandbox = new Sandbox(t, { home })
249287

250-
await sandbox.run('config', ['set', 'foo=baz'])
288+
await sandbox.run('config', ['set', 'access=restricted'])
251289

252-
t.equal(sandbox.config.get('foo'), 'baz', 'set the value for foo')
290+
t.equal(sandbox.config.get('access'), 'restricted', 'set the value for access')
253291

254-
const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' })
255-
t.ok(contents.includes('foo=baz'), 'wrote foo to disk')
292+
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
293+
const rc = ini.parse(contents)
294+
t.equal(rc.access, 'restricted', 'access is set to restricted')
256295
})
257296

258297
t.test('config set key1 value1 key2=value2 key3', async t => {
259298
const home = t.testdir({
260-
'.npmrc': 'foo=bar\nbar=baz\nbaz=foo',
299+
'.npmrc': 'access=public\nall=true\naudit=true',
261300
})
262301

263302
const sandbox = new Sandbox(t, { home })
264-
await sandbox.run('config', ['set', 'foo', 'oof', 'bar=rab', 'baz'])
303+
await sandbox.run('config', ['set', 'access', 'restricted', 'all=false', 'audit'])
265304

266-
t.equal(sandbox.config.get('foo'), 'oof', 'foo was set')
267-
t.equal(sandbox.config.get('bar'), 'rab', 'bar was set')
268-
t.equal(sandbox.config.get('baz'), '', 'baz was set')
305+
t.equal(sandbox.config.get('access'), 'restricted', 'access was set')
306+
t.equal(sandbox.config.get('all'), false, 'all was set')
307+
t.equal(sandbox.config.get('audit'), false, 'audit was set')
269308

270-
const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' })
271-
t.ok(contents.includes('foo=oof'), 'foo was written to disk')
272-
t.ok(contents.includes('bar=rab'), 'bar was written to disk')
273-
t.ok(contents.includes('baz='), 'baz was written to disk')
309+
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
310+
const rc = ini.parse(contents)
311+
t.equal(rc.access, 'restricted', 'access is set to restricted')
312+
t.equal(rc.all, false, 'all is set to false')
313+
t.equal(rc.audit, false, 'audit is set to false')
274314
})
275315

276316
t.test('config set invalid key logs warning', async t => {
@@ -287,30 +327,32 @@ t.test('config set invalid key logs warning', async t => {
287327

288328
t.test('config set key=value --location=global', async t => {
289329
const global = t.testdir({
290-
npmrc: 'foo=bar\nbar=baz',
330+
npmrc: 'access=public\nall=true',
291331
})
292332

293333
const sandbox = new Sandbox(t, { global })
294-
await sandbox.run('config', ['set', 'foo=buzz', '--location=global'])
334+
await sandbox.run('config', ['set', 'access=restricted', '--location=global'])
295335

296-
t.equal(sandbox.config.get('foo', 'global'), 'buzz', 'foo should be set')
336+
t.equal(sandbox.config.get('access', 'global'), 'restricted', 'foo should be set')
297337

298-
const contents = await readFile(join(global, 'npmrc'), { encoding: 'utf8' })
299-
t.not(contents.includes('foo=buzz'), 'foo was saved on disk')
338+
const contents = await fs.readFile(join(global, 'npmrc'), { encoding: 'utf8' })
339+
const rc = ini.parse(contents)
340+
t.equal(rc.access, 'restricted', 'access is set to restricted')
300341
})
301342

302343
t.test('config set key=value --global', async t => {
303344
const global = t.testdir({
304-
npmrc: 'foo=bar\nbar=baz',
345+
npmrc: 'access=public\nall=true',
305346
})
306347

307348
const sandbox = new Sandbox(t, { global })
308-
await sandbox.run('config', ['set', 'foo=buzz', '--global'])
349+
await sandbox.run('config', ['set', 'access=restricted', '--global'])
309350

310-
t.equal(sandbox.config.get('foo', 'global'), 'buzz', 'foo should be set')
351+
t.equal(sandbox.config.get('access', 'global'), 'restricted', 'access should be set')
311352

312-
const contents = await readFile(join(global, 'npmrc'), { encoding: 'utf8' })
313-
t.not(contents.includes('foo=buzz'), 'foo was saved on disk')
353+
const contents = await fs.readFile(join(global, 'npmrc'), { encoding: 'utf8' })
354+
const rc = ini.parse(contents)
355+
t.equal(rc.access, 'restricted', 'access is set to restricted')
314356
})
315357

316358
t.test('config get no args', async t => {
@@ -383,7 +425,7 @@ t.test('config edit', async t => {
383425
'editor opened the user config file'
384426
)
385427

386-
const contents = await readFile(join(home, '.npmrc'), { encoding: 'utf8' })
428+
const contents = await fs.readFile(join(home, '.npmrc'), { encoding: 'utf8' })
387429
t.ok(contents.includes('foo=bar'), 'kept foo')
388430
t.ok(contents.includes('bar=baz'), 'kept bar')
389431
t.ok(contents.includes('shown below with default values'), 'appends defaults to file')
@@ -448,7 +490,7 @@ t.test('config fix', (t) => {
448490
t.not(sandbox.config.get('_authToken', 'global'), '_authToken is not set globally')
449491
t.equal(sandbox.config.get(`${registry}:_authToken`, 'global'), 'afaketoken',
450492
'global _authToken was scoped')
451-
const globalConfig = await readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' })
493+
const globalConfig = await fs.readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' })
452494
t.equal(globalConfig, `${registry}:_authToken=afaketoken\n`, 'global config was written')
453495

454496
// user config fixes
@@ -459,7 +501,7 @@ t.test('config fix', (t) => {
459501
t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config')
460502
t.not(sandbox.config.get('_auth'), '_auth is not set in user config')
461503
t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped')
462-
const userConfig = await readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' })
504+
const userConfig = await fs.readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' })
463505
t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written')
464506
})
465507

@@ -488,7 +530,7 @@ t.test('config fix', (t) => {
488530
t.equal(sandbox.config.get('_authtoken', 'global'), 'notatoken', 'global _authtoken untouched')
489531
t.equal(sandbox.config.get('_authToken', 'global'), 'afaketoken', 'global _authToken untouched')
490532
t.not(sandbox.config.get(`${registry}:_authToken`, 'global'), 'global _authToken not scoped')
491-
const globalConfig = await readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' })
533+
const globalConfig = await fs.readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' })
492534
t.equal(globalConfig, '_authtoken=notatoken\n_authToken=afaketoken',
493535
'global config was not written')
494536

@@ -500,7 +542,7 @@ t.test('config fix', (t) => {
500542
t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config')
501543
t.not(sandbox.config.get('_auth', 'user'), '_auth is not set in user config')
502544
t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped')
503-
const userConfig = await readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' })
545+
const userConfig = await fs.readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' })
504546
t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written')
505547
})
506548

0 commit comments

Comments
 (0)