@npmcli/config 4.2.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -89,6 +89,7 @@ process.on('log', (level, ...args) => {
89
89
  // returns a promise that fails if config loading fails, and
90
90
  // resolves when the config object is ready for action
91
91
  conf.load().then(() => {
92
+ conf.validate()
92
93
  console.log('loaded ok! some-key = ' + conf.get('some-key'))
93
94
  }).catch(er => {
94
95
  console.error('error loading configs!', er)
@@ -210,6 +211,10 @@ Delete the configuration key from the specified level in the config stack.
210
211
  Verify that all known configuration options are set to valid values, and
211
212
  log a warning if they are invalid.
212
213
 
214
+ Invalid auth options will cause this method to throw an error with a `code`
215
+ property of `ERR_INVALID_AUTH`, and a `problems` property listing the specific
216
+ concerns with the current configuration.
217
+
213
218
  If `where` is not set, then all config objects are validated.
214
219
 
215
220
  Returns `true` if all configs are valid.
@@ -218,6 +223,14 @@ Note that it's usually enough (and more efficient) to just check
218
223
  `config.valid`, since each data object is marked for re-evaluation on every
219
224
  `config.set()` operation.
220
225
 
226
+ ### `config.repair(problems)`
227
+
228
+ Accept an optional array of problems (as thrown by `config.validate()`) and
229
+ perform the necessary steps to resolve them. If no problems are provided,
230
+ this method will call `config.validate()` internally to retrieve them.
231
+
232
+ Note that you must `await config.save('user')` in order to persist the changes.
233
+
221
234
  ### `config.isDefault(key)`
222
235
 
223
236
  Returns `true` if the value is coming directly from the
package/lib/errors.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict'
2
+
3
+ class ErrInvalidAuth extends Error {
4
+ constructor (problems) {
5
+ let message = 'Invalid auth configuration found: '
6
+ message += problems.map((problem) => {
7
+ if (problem.action === 'delete') {
8
+ return `\`${problem.key}\` is not allowed in ${problem.where} config`
9
+ } else if (problem.action === 'rename') {
10
+ return `\`${problem.from}\` must be renamed to \`${problem.to}\` in ${problem.where} config`
11
+ }
12
+ }).join(', ')
13
+ message += '\nPlease run `npm config fix` to repair your configuration.`'
14
+ super(message)
15
+ this.code = 'ERR_INVALID_AUTH'
16
+ this.problems = problems
17
+ }
18
+ }
19
+
20
+ module.exports = {
21
+ ErrInvalidAuth,
22
+ }
package/lib/index.js CHANGED
@@ -51,6 +51,10 @@ const parseField = require('./parse-field.js')
51
51
  const typeDescription = require('./type-description.js')
52
52
  const setEnvs = require('./set-envs.js')
53
53
 
54
+ const {
55
+ ErrInvalidAuth,
56
+ } = require('./errors.js')
57
+
54
58
  // types that can be saved back to
55
59
  const confFileTypes = new Set([
56
60
  'global',
@@ -280,26 +284,10 @@ class Config {
280
284
  await this.loadGlobalConfig()
281
285
  process.emit('timeEnd', 'config:load:global')
282
286
 
283
- // warn if anything is not valid
284
- process.emit('time', 'config:load:validate')
285
- this.validate()
286
- process.emit('timeEnd', 'config:load:validate')
287
-
288
287
  // set this before calling setEnvs, so that we don't have to share
289
288
  // symbols, as that module also does a bunch of get operations
290
289
  this[_loaded] = true
291
290
 
292
- process.emit('time', 'config:load:credentials')
293
- const reg = this.get('registry')
294
- const creds = this.getCredentialsByURI(reg)
295
- // ignore this error because a failed set will strip out anything that
296
- // might be a security hazard, which was the intention.
297
- try {
298
- this.setCredentialsByURI(reg, creds)
299
- // eslint-disable-next-line no-empty
300
- } catch (_) {}
301
- process.emit('timeEnd', 'config:load:credentials')
302
-
303
291
  // set proper globalPrefix now that everything is loaded
304
292
  this.globalPrefix = this.get('prefix')
305
293
 
@@ -399,7 +387,9 @@ class Config {
399
387
  validate (where) {
400
388
  if (!where) {
401
389
  let valid = true
402
- for (const [entryWhere] of this.data.entries()) {
390
+ const authProblems = []
391
+
392
+ for (const entryWhere of this.data.keys()) {
403
393
  // no need to validate our defaults, we know they're fine
404
394
  // cli was already validated when parsed the first time
405
395
  if (entryWhere === 'default' || entryWhere === 'builtin' || entryWhere === 'cli') {
@@ -407,7 +397,48 @@ class Config {
407
397
  }
408
398
  const ret = this.validate(entryWhere)
409
399
  valid = valid && ret
400
+
401
+ if (['global', 'user', 'project'].includes(entryWhere)) {
402
+ // after validating everything else, we look for old auth configs we no longer support
403
+ // if these keys are found, we build up a list of them and the appropriate action and
404
+ // attach it as context on the thrown error
405
+
406
+ // first, keys that should be removed
407
+ for (const key of ['_authtoken', '-authtoken']) {
408
+ if (this.get(key, entryWhere)) {
409
+ authProblems.push({ action: 'delete', key, where: entryWhere })
410
+ }
411
+ }
412
+
413
+ // NOTE we pull registry without restricting to the current 'where' because we want to
414
+ // suggest scoping things to the registry they would be applied to, which is the default
415
+ // regardless of where it was defined
416
+ const nerfedReg = nerfDart(this.get('registry'))
417
+ // keys that should be nerfed but currently are not
418
+ for (const key of ['_auth', '_authToken', 'username', '_password']) {
419
+ if (this.get(key, entryWhere)) {
420
+ // username and _password must both exist in the same file to be recognized correctly
421
+ if (key === 'username' && !this.get('_password', entryWhere)) {
422
+ authProblems.push({ action: 'delete', key, where: entryWhere })
423
+ } else if (key === '_password' && !this.get('username', entryWhere)) {
424
+ authProblems.push({ action: 'delete', key, where: entryWhere })
425
+ } else {
426
+ authProblems.push({
427
+ action: 'rename',
428
+ from: key,
429
+ to: `${nerfedReg}:${key}`,
430
+ where: entryWhere,
431
+ })
432
+ }
433
+ }
434
+ }
435
+ }
410
436
  }
437
+
438
+ if (authProblems.length) {
439
+ throw new ErrInvalidAuth(authProblems)
440
+ }
441
+
411
442
  return valid
412
443
  } else {
413
444
  const obj = this.data.get(where)
@@ -423,6 +454,40 @@ class Config {
423
454
  }
424
455
  }
425
456
 
457
+ // fixes problems identified by validate(), accepts the 'problems' property from a thrown
458
+ // ErrInvalidAuth to avoid having to check everything again
459
+ repair (problems) {
460
+ if (!problems) {
461
+ try {
462
+ this.validate()
463
+ } catch (err) {
464
+ // coverage skipped here because we don't need to test re-throwing an error
465
+ // istanbul ignore next
466
+ if (err.code !== 'ERR_INVALID_AUTH') {
467
+ throw err
468
+ }
469
+
470
+ problems = err.problems
471
+ } finally {
472
+ if (!problems) {
473
+ problems = []
474
+ }
475
+ }
476
+ }
477
+
478
+ for (const problem of problems) {
479
+ // coverage disabled for else branch because it doesn't do anything and shouldn't
480
+ // istanbul ignore else
481
+ if (problem.action === 'delete') {
482
+ this.delete(problem.key, problem.where)
483
+ } else if (problem.action === 'rename') {
484
+ const old = this.get(problem.from, problem.where)
485
+ this.set(problem.to, old, problem.where)
486
+ this.delete(problem.from, problem.where)
487
+ }
488
+ }
489
+ }
490
+
426
491
  // Returns true if the value is coming directly from the source defined
427
492
  // in default definitions, if the current value for the key config is
428
493
  // coming from any other different source, returns false
@@ -644,21 +709,19 @@ class Config {
644
709
  if (!confFileTypes.has(where)) {
645
710
  throw new Error('invalid config location param: ' + where)
646
711
  }
712
+
647
713
  const conf = this.data.get(where)
648
714
  conf[_raw] = { ...conf.data }
649
715
  conf[_loadError] = null
650
716
 
651
- // upgrade auth configs to more secure variants before saving
652
717
  if (where === 'user') {
653
- const reg = this.get('registry')
654
- const creds = this.getCredentialsByURI(reg)
655
- // we ignore this error because the failed set already removed
656
- // anything that might be a security hazard, and it won't be
657
- // saved back to the .npmrc file, so we're good.
658
- try {
659
- this.setCredentialsByURI(reg, creds)
660
- // eslint-disable-next-line no-empty
661
- } catch (_) {}
718
+ // if email is nerfed, then we want to de-nerf it
719
+ const nerfed = nerfDart(this.get('registry'))
720
+ const email = this.get(`${nerfed}:email`, 'user')
721
+ if (email) {
722
+ this.delete(`${nerfed}:email`, 'user')
723
+ this.set('email', email, 'user')
724
+ }
662
725
  }
663
726
 
664
727
  const iniData = ini.stringify(conf.data).trim() + '\n'
@@ -686,14 +749,17 @@ class Config {
686
749
  const nerfed = nerfDart(uri)
687
750
  const def = nerfDart(this.get('registry'))
688
751
  if (def === nerfed) {
689
- // do not delete email, that shouldn't be nerfed any more.
690
- // just delete the nerfed copy, if one exists.
691
752
  this.delete(`-authtoken`, 'user')
692
753
  this.delete(`_authToken`, 'user')
693
754
  this.delete(`_authtoken`, 'user')
694
755
  this.delete(`_auth`, 'user')
695
756
  this.delete(`_password`, 'user')
696
757
  this.delete(`username`, 'user')
758
+ // de-nerf email if it's nerfed to the default registry
759
+ const email = this.get(`${nerfed}:email`, 'user')
760
+ if (email) {
761
+ this.set('email', email, 'user')
762
+ }
697
763
  }
698
764
  this.delete(`${nerfed}:_authToken`, 'user')
699
765
  this.delete(`${nerfed}:_auth`, 'user')
@@ -706,28 +772,9 @@ class Config {
706
772
 
707
773
  setCredentialsByURI (uri, { token, username, password, email, certfile, keyfile }) {
708
774
  const nerfed = nerfDart(uri)
709
- const def = nerfDart(this.get('registry'))
710
775
 
711
- if (def === nerfed) {
712
- // remove old style auth info not limited to a single registry
713
- this.delete('_password', 'user')
714
- this.delete('username', 'user')
715
- this.delete('_auth', 'user')
716
- this.delete('_authtoken', 'user')
717
- this.delete('-authtoken', 'user')
718
- this.delete('_authToken', 'user')
719
- }
720
-
721
- // email used to be nerfed always. if we're using the default
722
- // registry, de-nerf it.
723
- if (nerfed === def) {
724
- email = email ||
725
- this.get('email', 'user') ||
726
- this.get(`${nerfed}:email`, 'user')
727
- if (email) {
728
- this.set('email', email, 'user')
729
- }
730
- }
776
+ // email is either provided, a top level key, or nothing
777
+ email = email || this.get('email', 'user')
731
778
 
732
779
  // field that hasn't been used as documented for a LONG time,
733
780
  // and as of npm 7.10.0, isn't used at all. We just always
@@ -765,15 +812,17 @@ class Config {
765
812
  // this has to be a bit more complicated to support legacy data of all forms
766
813
  getCredentialsByURI (uri) {
767
814
  const nerfed = nerfDart(uri)
815
+ const def = nerfDart(this.get('registry'))
768
816
  const creds = {}
769
817
 
770
- const deprecatedAuthWarning = [
771
- '`_auth`, `_authToken`, `username` and `_password` must be scoped to a registry.',
772
- 'see `npm help npmrc` for more information.',
773
- ].join(' ')
774
-
818
+ // email is handled differently, it used to always be nerfed and now it never should be
819
+ // if it's set nerfed to the default registry, then we copy it to the unnerfed key
820
+ // TODO: evaluate removing 'email' from the credentials object returned here
775
821
  const email = this.get(`${nerfed}:email`) || this.get('email')
776
822
  if (email) {
823
+ if (nerfed === def) {
824
+ this.set('email', email, 'user')
825
+ }
777
826
  creds.email = email
778
827
  }
779
828
 
@@ -785,13 +834,8 @@ class Config {
785
834
  // cert/key may be used in conjunction with other credentials, thus no `return`
786
835
  }
787
836
 
788
- const defaultToken = nerfDart(this.get('registry')) && this.get('_authToken')
789
- const tokenReg = this.get(`${nerfed}:_authToken`) || defaultToken
790
-
837
+ const tokenReg = this.get(`${nerfed}:_authToken`)
791
838
  if (tokenReg) {
792
- if (tokenReg === defaultToken) {
793
- log.warn('config', deprecatedAuthWarning)
794
- }
795
839
  creds.token = tokenReg
796
840
  return creds
797
841
  }
@@ -816,37 +860,7 @@ class Config {
816
860
  return creds
817
861
  }
818
862
 
819
- // at this point, we can only use the values if the URI is the
820
- // default registry.
821
- const defaultNerf = nerfDart(this.get('registry'))
822
- if (nerfed !== defaultNerf) {
823
- return creds
824
- }
825
-
826
- const userDef = this.get('username')
827
- const passDef = this.get('_password')
828
- if (userDef && passDef) {
829
- log.warn('config', deprecatedAuthWarning)
830
- creds.username = userDef
831
- creds.password = Buffer.from(passDef, 'base64').toString('utf8')
832
- const auth = `${creds.username}:${creds.password}`
833
- creds.auth = Buffer.from(auth, 'utf8').toString('base64')
834
- return creds
835
- }
836
-
837
- // Handle the old-style _auth=<base64> style for the default
838
- // registry, if set.
839
- const auth = this.get('_auth')
840
- if (!auth) {
841
- return creds
842
- }
843
-
844
- log.warn('config', deprecatedAuthWarning)
845
- const authDecode = Buffer.from(auth, 'base64').toString('utf8')
846
- const authSplit = authDecode.split(':')
847
- creds.username = authSplit.shift()
848
- creds.password = authSplit.join(':')
849
- creds.auth = auth
863
+ // at this point, nothing else is usable so just return what we do have
850
864
  return creds
851
865
  }
852
866
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/config",
3
- "version": "4.2.2",
3
+ "version": "5.0.0",
4
4
  "files": [
5
5
  "bin/",
6
6
  "lib/"
@@ -16,9 +16,6 @@
16
16
  "scripts": {
17
17
  "test": "tap",
18
18
  "snap": "tap",
19
- "preversion": "npm test",
20
- "postversion": "npm publish",
21
- "prepublishOnly": "git push origin --follow-tags",
22
19
  "lint": "eslint \"**/*.js\"",
23
20
  "postlint": "template-oss-check",
24
21
  "lintfix": "npm run lint -- --fix",
@@ -27,11 +24,15 @@
27
24
  },
28
25
  "tap": {
29
26
  "check-coverage": true,
30
- "coverage-map": "map.js"
27
+ "coverage-map": "map.js",
28
+ "nyc-arg": [
29
+ "--exclude",
30
+ "tap-snapshots/**"
31
+ ]
31
32
  },
32
33
  "devDependencies": {
33
34
  "@npmcli/eslint-config": "^3.0.1",
34
- "@npmcli/template-oss": "3.6.0",
35
+ "@npmcli/template-oss": "4.5.0",
35
36
  "tap": "^16.0.1"
36
37
  },
37
38
  "dependencies": {
@@ -45,10 +46,10 @@
45
46
  "walk-up-path": "^1.0.0"
46
47
  },
47
48
  "engines": {
48
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
49
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
49
50
  },
50
51
  "templateOSS": {
51
52
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
52
- "version": "3.6.0"
53
+ "version": "4.5.0"
53
54
  }
54
55
  }