@npmcli/config 4.2.2 → 6.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
@@ -2,26 +2,20 @@
2
2
  const walkUp = require('walk-up-path')
3
3
  const ini = require('ini')
4
4
  const nopt = require('nopt')
5
- const mkdirp = require('mkdirp-infer-owner')
6
5
  const mapWorkspaces = require('@npmcli/map-workspaces')
7
6
  const rpj = require('read-package-json-fast')
8
7
  const log = require('proc-log')
9
8
 
10
- /* istanbul ignore next */
11
- const myUid = process.getuid && process.getuid()
12
- /* istanbul ignore next */
13
- const myGid = process.getgid && process.getgid()
14
-
15
9
  const { resolve, dirname, join } = require('path')
16
10
  const { homedir } = require('os')
17
- const { promisify } = require('util')
18
- const fs = require('fs')
19
- const readFile = promisify(fs.readFile)
20
- const writeFile = promisify(fs.writeFile)
21
- const chmod = promisify(fs.chmod)
22
- const chown = promisify(fs.chown)
23
- const unlink = promisify(fs.unlink)
24
- const stat = promisify(fs.stat)
11
+ const {
12
+ readFile,
13
+ writeFile,
14
+ chmod,
15
+ unlink,
16
+ stat,
17
+ mkdir,
18
+ } = require('fs/promises')
25
19
 
26
20
  const hasOwnProperty = (obj, key) =>
27
21
  Object.prototype.hasOwnProperty.call(obj, key)
@@ -51,6 +45,10 @@ const parseField = require('./parse-field.js')
51
45
  const typeDescription = require('./type-description.js')
52
46
  const setEnvs = require('./set-envs.js')
53
47
 
48
+ const {
49
+ ErrInvalidAuth,
50
+ } = require('./errors.js')
51
+
54
52
  // types that can be saved back to
55
53
  const confFileTypes = new Set([
56
54
  'global',
@@ -280,26 +278,10 @@ class Config {
280
278
  await this.loadGlobalConfig()
281
279
  process.emit('timeEnd', 'config:load:global')
282
280
 
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
281
  // set this before calling setEnvs, so that we don't have to share
289
282
  // symbols, as that module also does a bunch of get operations
290
283
  this[_loaded] = true
291
284
 
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
285
  // set proper globalPrefix now that everything is loaded
304
286
  this.globalPrefix = this.get('prefix')
305
287
 
@@ -399,7 +381,9 @@ class Config {
399
381
  validate (where) {
400
382
  if (!where) {
401
383
  let valid = true
402
- for (const [entryWhere] of this.data.entries()) {
384
+ const authProblems = []
385
+
386
+ for (const entryWhere of this.data.keys()) {
403
387
  // no need to validate our defaults, we know they're fine
404
388
  // cli was already validated when parsed the first time
405
389
  if (entryWhere === 'default' || entryWhere === 'builtin' || entryWhere === 'cli') {
@@ -407,7 +391,48 @@ class Config {
407
391
  }
408
392
  const ret = this.validate(entryWhere)
409
393
  valid = valid && ret
394
+
395
+ if (['global', 'user', 'project'].includes(entryWhere)) {
396
+ // after validating everything else, we look for old auth configs we no longer support
397
+ // if these keys are found, we build up a list of them and the appropriate action and
398
+ // attach it as context on the thrown error
399
+
400
+ // first, keys that should be removed
401
+ for (const key of ['_authtoken', '-authtoken']) {
402
+ if (this.get(key, entryWhere)) {
403
+ authProblems.push({ action: 'delete', key, where: entryWhere })
404
+ }
405
+ }
406
+
407
+ // NOTE we pull registry without restricting to the current 'where' because we want to
408
+ // suggest scoping things to the registry they would be applied to, which is the default
409
+ // regardless of where it was defined
410
+ const nerfedReg = nerfDart(this.get('registry'))
411
+ // keys that should be nerfed but currently are not
412
+ for (const key of ['_auth', '_authToken', 'username', '_password']) {
413
+ if (this.get(key, entryWhere)) {
414
+ // username and _password must both exist in the same file to be recognized correctly
415
+ if (key === 'username' && !this.get('_password', entryWhere)) {
416
+ authProblems.push({ action: 'delete', key, where: entryWhere })
417
+ } else if (key === '_password' && !this.get('username', entryWhere)) {
418
+ authProblems.push({ action: 'delete', key, where: entryWhere })
419
+ } else {
420
+ authProblems.push({
421
+ action: 'rename',
422
+ from: key,
423
+ to: `${nerfedReg}:${key}`,
424
+ where: entryWhere,
425
+ })
426
+ }
427
+ }
428
+ }
429
+ }
430
+ }
431
+
432
+ if (authProblems.length) {
433
+ throw new ErrInvalidAuth(authProblems)
410
434
  }
435
+
411
436
  return valid
412
437
  } else {
413
438
  const obj = this.data.get(where)
@@ -423,6 +448,40 @@ class Config {
423
448
  }
424
449
  }
425
450
 
451
+ // fixes problems identified by validate(), accepts the 'problems' property from a thrown
452
+ // ErrInvalidAuth to avoid having to check everything again
453
+ repair (problems) {
454
+ if (!problems) {
455
+ try {
456
+ this.validate()
457
+ } catch (err) {
458
+ // coverage skipped here because we don't need to test re-throwing an error
459
+ // istanbul ignore next
460
+ if (err.code !== 'ERR_INVALID_AUTH') {
461
+ throw err
462
+ }
463
+
464
+ problems = err.problems
465
+ } finally {
466
+ if (!problems) {
467
+ problems = []
468
+ }
469
+ }
470
+ }
471
+
472
+ for (const problem of problems) {
473
+ // coverage disabled for else branch because it doesn't do anything and shouldn't
474
+ // istanbul ignore else
475
+ if (problem.action === 'delete') {
476
+ this.delete(problem.key, problem.where)
477
+ } else if (problem.action === 'rename') {
478
+ const old = this.get(problem.from, problem.where)
479
+ this.set(problem.to, old, problem.where)
480
+ this.delete(problem.from, problem.where)
481
+ }
482
+ }
483
+ }
484
+
426
485
  // Returns true if the value is coming directly from the source defined
427
486
  // in default definitions, if the current value for the key config is
428
487
  // coming from any other different source, returns false
@@ -644,21 +703,19 @@ class Config {
644
703
  if (!confFileTypes.has(where)) {
645
704
  throw new Error('invalid config location param: ' + where)
646
705
  }
706
+
647
707
  const conf = this.data.get(where)
648
708
  conf[_raw] = { ...conf.data }
649
709
  conf[_loadError] = null
650
710
 
651
- // upgrade auth configs to more secure variants before saving
652
711
  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 (_) {}
712
+ // if email is nerfed, then we want to de-nerf it
713
+ const nerfed = nerfDart(this.get('registry'))
714
+ const email = this.get(`${nerfed}:email`, 'user')
715
+ if (email) {
716
+ this.delete(`${nerfed}:email`, 'user')
717
+ this.set('email', email, 'user')
718
+ }
662
719
  }
663
720
 
664
721
  const iniData = ini.stringify(conf.data).trim() + '\n'
@@ -668,16 +725,8 @@ class Config {
668
725
  return
669
726
  }
670
727
  const dir = dirname(conf.source)
671
- await mkdirp(dir)
728
+ await mkdir(dir, { recursive: true })
672
729
  await writeFile(conf.source, iniData, 'utf8')
673
- // don't leave a root-owned config file lying around
674
- /* istanbul ignore if - this is best-effort and a pita to test */
675
- if (myUid === 0) {
676
- const st = await stat(dir).catch(() => null)
677
- if (st && (st.uid !== myUid || st.gid !== myGid)) {
678
- await chown(conf.source, st.uid, st.gid).catch(() => {})
679
- }
680
- }
681
730
  const mode = where === 'user' ? 0o600 : 0o666
682
731
  await chmod(conf.source, mode)
683
732
  }
@@ -686,14 +735,17 @@ class Config {
686
735
  const nerfed = nerfDart(uri)
687
736
  const def = nerfDart(this.get('registry'))
688
737
  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
738
  this.delete(`-authtoken`, 'user')
692
739
  this.delete(`_authToken`, 'user')
693
740
  this.delete(`_authtoken`, 'user')
694
741
  this.delete(`_auth`, 'user')
695
742
  this.delete(`_password`, 'user')
696
743
  this.delete(`username`, 'user')
744
+ // de-nerf email if it's nerfed to the default registry
745
+ const email = this.get(`${nerfed}:email`, 'user')
746
+ if (email) {
747
+ this.set('email', email, 'user')
748
+ }
697
749
  }
698
750
  this.delete(`${nerfed}:_authToken`, 'user')
699
751
  this.delete(`${nerfed}:_auth`, 'user')
@@ -706,28 +758,9 @@ class Config {
706
758
 
707
759
  setCredentialsByURI (uri, { token, username, password, email, certfile, keyfile }) {
708
760
  const nerfed = nerfDart(uri)
709
- const def = nerfDart(this.get('registry'))
710
761
 
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
- }
762
+ // email is either provided, a top level key, or nothing
763
+ email = email || this.get('email', 'user')
731
764
 
732
765
  // field that hasn't been used as documented for a LONG time,
733
766
  // and as of npm 7.10.0, isn't used at all. We just always
@@ -765,15 +798,17 @@ class Config {
765
798
  // this has to be a bit more complicated to support legacy data of all forms
766
799
  getCredentialsByURI (uri) {
767
800
  const nerfed = nerfDart(uri)
801
+ const def = nerfDart(this.get('registry'))
768
802
  const creds = {}
769
803
 
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
-
804
+ // email is handled differently, it used to always be nerfed and now it never should be
805
+ // if it's set nerfed to the default registry, then we copy it to the unnerfed key
806
+ // TODO: evaluate removing 'email' from the credentials object returned here
775
807
  const email = this.get(`${nerfed}:email`) || this.get('email')
776
808
  if (email) {
809
+ if (nerfed === def) {
810
+ this.set('email', email, 'user')
811
+ }
777
812
  creds.email = email
778
813
  }
779
814
 
@@ -785,13 +820,8 @@ class Config {
785
820
  // cert/key may be used in conjunction with other credentials, thus no `return`
786
821
  }
787
822
 
788
- const defaultToken = nerfDart(this.get('registry')) && this.get('_authToken')
789
- const tokenReg = this.get(`${nerfed}:_authToken`) || defaultToken
790
-
823
+ const tokenReg = this.get(`${nerfed}:_authToken`)
791
824
  if (tokenReg) {
792
- if (tokenReg === defaultToken) {
793
- log.warn('config', deprecatedAuthWarning)
794
- }
795
825
  creds.token = tokenReg
796
826
  return creds
797
827
  }
@@ -816,37 +846,7 @@ class Config {
816
846
  return creds
817
847
  }
818
848
 
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
849
+ // at this point, nothing else is usable so just return what we do have
850
850
  return creds
851
851
  }
852
852
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/config",
3
- "version": "4.2.2",
3
+ "version": "6.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,28 +24,31 @@
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.1",
35
36
  "tap": "^16.0.1"
36
37
  },
37
38
  "dependencies": {
38
39
  "@npmcli/map-workspaces": "^2.0.2",
39
40
  "ini": "^3.0.0",
40
- "mkdirp-infer-owner": "^2.0.0",
41
41
  "nopt": "^6.0.0",
42
42
  "proc-log": "^2.0.0",
43
- "read-package-json-fast": "^2.0.3",
43
+ "read-package-json-fast": "^3.0.0",
44
44
  "semver": "^7.3.5",
45
45
  "walk-up-path": "^1.0.0"
46
46
  },
47
47
  "engines": {
48
- "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
48
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
49
49
  },
50
50
  "templateOSS": {
51
51
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
52
- "version": "3.6.0"
52
+ "version": "4.5.1"
53
53
  }
54
54
  }