1// TODO: set the scope config from package.json or explicit cli config 2const { walkUp } = require('walk-up-path') 3const ini = require('ini') 4const nopt = require('nopt') 5const mapWorkspaces = require('@npmcli/map-workspaces') 6const rpj = require('read-package-json-fast') 7const log = require('proc-log') 8 9const { resolve, dirname, join } = require('path') 10const { homedir } = require('os') 11const { 12 readFile, 13 writeFile, 14 chmod, 15 unlink, 16 stat, 17 mkdir, 18} = require('fs/promises') 19 20const fileExists = (...p) => stat(resolve(...p)) 21 .then((st) => st.isFile()) 22 .catch(() => false) 23 24const dirExists = (...p) => stat(resolve(...p)) 25 .then((st) => st.isDirectory()) 26 .catch(() => false) 27 28const hasOwnProperty = (obj, key) => 29 Object.prototype.hasOwnProperty.call(obj, key) 30 31// define a custom getter, but turn into a normal prop 32// if we set it. otherwise it can't be set on child objects 33const settableGetter = (obj, key, get) => { 34 Object.defineProperty(obj, key, { 35 get, 36 set (value) { 37 Object.defineProperty(obj, key, { 38 value, 39 configurable: true, 40 writable: true, 41 enumerable: true, 42 }) 43 }, 44 configurable: true, 45 enumerable: true, 46 }) 47} 48 49const typeDefs = require('./type-defs.js') 50const nerfDart = require('./nerf-dart.js') 51const envReplace = require('./env-replace.js') 52const parseField = require('./parse-field.js') 53const typeDescription = require('./type-description.js') 54const setEnvs = require('./set-envs.js') 55 56const { 57 ErrInvalidAuth, 58} = require('./errors.js') 59 60// types that can be saved back to 61const confFileTypes = new Set([ 62 'global', 63 'user', 64 'project', 65]) 66 67const confTypes = new Set([ 68 'default', 69 'builtin', 70 ...confFileTypes, 71 'env', 72 'cli', 73]) 74 75class Config { 76 #loaded = false 77 #flatten 78 // populated the first time we flatten the object 79 #flatOptions = null 80 81 static get typeDefs () { 82 return typeDefs 83 } 84 85 constructor ({ 86 definitions, 87 shorthands, 88 flatten, 89 npmPath, 90 91 // options just to override in tests, mostly 92 env = process.env, 93 argv = process.argv, 94 platform = process.platform, 95 execPath = process.execPath, 96 cwd = process.cwd(), 97 excludeNpmCwd = false, 98 }) { 99 // turn the definitions into nopt's weirdo syntax 100 this.definitions = definitions 101 const types = {} 102 const defaults = {} 103 this.deprecated = {} 104 for (const [key, def] of Object.entries(definitions)) { 105 defaults[key] = def.default 106 types[key] = def.type 107 if (def.deprecated) { 108 this.deprecated[key] = def.deprecated.trim().replace(/\n +/, '\n') 109 } 110 } 111 112 this.#flatten = flatten 113 this.types = types 114 this.shorthands = shorthands 115 this.defaults = defaults 116 117 this.npmPath = npmPath 118 this.npmBin = join(this.npmPath, 'bin/npm-cli.js') 119 this.argv = argv 120 this.env = env 121 this.execPath = execPath 122 this.platform = platform 123 this.cwd = cwd 124 this.excludeNpmCwd = excludeNpmCwd 125 126 // set when we load configs 127 this.globalPrefix = null 128 this.localPrefix = null 129 this.localPackage = null 130 131 // defaults to env.HOME, but will always be *something* 132 this.home = null 133 134 // set up the prototype chain of config objects 135 const wheres = [...confTypes] 136 this.data = new Map() 137 let parent = null 138 for (const where of wheres) { 139 this.data.set(where, parent = new ConfigData(parent)) 140 } 141 142 this.data.set = () => { 143 throw new Error('cannot change internal config data structure') 144 } 145 this.data.delete = () => { 146 throw new Error('cannot change internal config data structure') 147 } 148 149 this.sources = new Map([]) 150 151 this.list = [] 152 for (const { data } of this.data.values()) { 153 this.list.unshift(data) 154 } 155 Object.freeze(this.list) 156 157 this.#loaded = false 158 } 159 160 get loaded () { 161 return this.#loaded 162 } 163 164 get prefix () { 165 return this.#get('global') ? this.globalPrefix : this.localPrefix 166 } 167 168 // return the location where key is found. 169 find (key) { 170 if (!this.loaded) { 171 throw new Error('call config.load() before reading values') 172 } 173 174 // have to look in reverse order 175 const entries = [...this.data.entries()] 176 for (let i = entries.length - 1; i > -1; i--) { 177 const [where, { data }] = entries[i] 178 if (hasOwnProperty(data, key)) { 179 return where 180 } 181 } 182 return null 183 } 184 185 get (key, where) { 186 if (!this.loaded) { 187 throw new Error('call config.load() before reading values') 188 } 189 return this.#get(key, where) 190 } 191 192 // we need to get values sometimes, so use this internal one to do so 193 // while in the process of loading. 194 #get (key, where = null) { 195 if (where !== null && !confTypes.has(where)) { 196 throw new Error('invalid config location param: ' + where) 197 } 198 const { data } = this.data.get(where || 'cli') 199 return where === null || hasOwnProperty(data, key) ? data[key] : undefined 200 } 201 202 set (key, val, where = 'cli') { 203 if (!this.loaded) { 204 throw new Error('call config.load() before setting values') 205 } 206 if (!confTypes.has(where)) { 207 throw new Error('invalid config location param: ' + where) 208 } 209 this.#checkDeprecated(key) 210 const { data, raw } = this.data.get(where) 211 data[key] = val 212 if (['global', 'user', 'project'].includes(where)) { 213 raw[key] = val 214 } 215 216 // this is now dirty, the next call to this.valid will have to check it 217 this.data.get(where)[_valid] = null 218 219 // the flat options are invalidated, regenerate next time they're needed 220 this.#flatOptions = null 221 } 222 223 get flat () { 224 if (this.#flatOptions) { 225 return this.#flatOptions 226 } 227 228 // create the object for flat options passed to deps 229 process.emit('time', 'config:load:flatten') 230 this.#flatOptions = {} 231 // walk from least priority to highest 232 for (const { data } of this.data.values()) { 233 this.#flatten(data, this.#flatOptions) 234 } 235 this.#flatOptions.nodeBin = this.execPath 236 this.#flatOptions.npmBin = this.npmBin 237 process.emit('timeEnd', 'config:load:flatten') 238 239 return this.#flatOptions 240 } 241 242 delete (key, where = 'cli') { 243 if (!this.loaded) { 244 throw new Error('call config.load() before deleting values') 245 } 246 if (!confTypes.has(where)) { 247 throw new Error('invalid config location param: ' + where) 248 } 249 const { data, raw } = this.data.get(where) 250 delete data[key] 251 if (['global', 'user', 'project'].includes(where)) { 252 delete raw[key] 253 } 254 } 255 256 async load () { 257 if (this.loaded) { 258 throw new Error('attempting to load npm config multiple times') 259 } 260 261 process.emit('time', 'config:load') 262 // first load the defaults, which sets the global prefix 263 process.emit('time', 'config:load:defaults') 264 this.loadDefaults() 265 process.emit('timeEnd', 'config:load:defaults') 266 267 // next load the builtin config, as this sets new effective defaults 268 process.emit('time', 'config:load:builtin') 269 await this.loadBuiltinConfig() 270 process.emit('timeEnd', 'config:load:builtin') 271 272 // cli and env are not async, and can set the prefix, relevant to project 273 process.emit('time', 'config:load:cli') 274 this.loadCLI() 275 process.emit('timeEnd', 'config:load:cli') 276 process.emit('time', 'config:load:env') 277 this.loadEnv() 278 process.emit('timeEnd', 'config:load:env') 279 280 // next project config, which can affect userconfig location 281 process.emit('time', 'config:load:project') 282 await this.loadProjectConfig() 283 process.emit('timeEnd', 'config:load:project') 284 // then user config, which can affect globalconfig location 285 process.emit('time', 'config:load:user') 286 await this.loadUserConfig() 287 process.emit('timeEnd', 'config:load:user') 288 // last but not least, global config file 289 process.emit('time', 'config:load:global') 290 await this.loadGlobalConfig() 291 process.emit('timeEnd', 'config:load:global') 292 293 // set this before calling setEnvs, so that we don't have to share 294 // private attributes, as that module also does a bunch of get operations 295 this.#loaded = true 296 297 // set proper globalPrefix now that everything is loaded 298 this.globalPrefix = this.get('prefix') 299 300 process.emit('time', 'config:load:setEnvs') 301 this.setEnvs() 302 process.emit('timeEnd', 'config:load:setEnvs') 303 304 process.emit('timeEnd', 'config:load') 305 } 306 307 loadDefaults () { 308 this.loadGlobalPrefix() 309 this.loadHome() 310 311 const defaultsObject = { 312 ...this.defaults, 313 prefix: this.globalPrefix, 314 } 315 316 try { 317 defaultsObject['npm-version'] = require(join(this.npmPath, 'package.json')).version 318 } catch { 319 // in some weird state where the passed in npmPath does not have a package.json 320 // this will never happen in npm, but is guarded here in case this is consumed 321 // in other ways + tests 322 } 323 324 this.#loadObject(defaultsObject, 'default', 'default values') 325 326 const { data } = this.data.get('default') 327 328 // if the prefix is set on cli, env, or userconfig, then we need to 329 // default the globalconfig file to that location, instead of the default 330 // global prefix. It's weird that `npm get globalconfig --prefix=/foo` 331 // returns `/foo/etc/npmrc`, but better to not change it at this point. 332 settableGetter(data, 'globalconfig', () => resolve(this.#get('prefix'), 'etc/npmrc')) 333 } 334 335 loadHome () { 336 this.home = this.env.HOME || homedir() 337 } 338 339 loadGlobalPrefix () { 340 if (this.globalPrefix) { 341 throw new Error('cannot load default global prefix more than once') 342 } 343 344 if (this.env.PREFIX) { 345 this.globalPrefix = this.env.PREFIX 346 } else if (this.platform === 'win32') { 347 // c:\node\node.exe --> prefix=c:\node\ 348 this.globalPrefix = dirname(this.execPath) 349 } else { 350 // /usr/local/bin/node --> prefix=/usr/local 351 this.globalPrefix = dirname(dirname(this.execPath)) 352 353 // destdir only is respected on Unix 354 if (this.env.DESTDIR) { 355 this.globalPrefix = join(this.env.DESTDIR, this.globalPrefix) 356 } 357 } 358 } 359 360 loadEnv () { 361 const conf = Object.create(null) 362 for (const [envKey, envVal] of Object.entries(this.env)) { 363 if (!/^npm_config_/i.test(envKey) || envVal === '') { 364 continue 365 } 366 let key = envKey.slice('npm_config_'.length) 367 if (!key.startsWith('//')) { // don't normalize nerf-darted keys 368 key = key.replace(/(?!^)_/g, '-') // don't replace _ at the start of the key 369 .toLowerCase() 370 } 371 conf[key] = envVal 372 } 373 this.#loadObject(conf, 'env', 'environment') 374 } 375 376 loadCLI () { 377 nopt.invalidHandler = (k, val, type) => 378 this.invalidHandler(k, val, type, 'command line options', 'cli') 379 const conf = nopt(this.types, this.shorthands, this.argv) 380 nopt.invalidHandler = null 381 this.parsedArgv = conf.argv 382 delete conf.argv 383 this.#loadObject(conf, 'cli', 'command line options') 384 } 385 386 get valid () { 387 for (const [where, { valid }] of this.data.entries()) { 388 if (valid === false || valid === null && !this.validate(where)) { 389 return false 390 } 391 } 392 return true 393 } 394 395 validate (where) { 396 if (!where) { 397 let valid = true 398 const authProblems = [] 399 400 for (const entryWhere of this.data.keys()) { 401 // no need to validate our defaults, we know they're fine 402 // cli was already validated when parsed the first time 403 if (entryWhere === 'default' || entryWhere === 'builtin' || entryWhere === 'cli') { 404 continue 405 } 406 const ret = this.validate(entryWhere) 407 valid = valid && ret 408 409 if (['global', 'user', 'project'].includes(entryWhere)) { 410 // after validating everything else, we look for old auth configs we no longer support 411 // if these keys are found, we build up a list of them and the appropriate action and 412 // attach it as context on the thrown error 413 414 // first, keys that should be removed 415 for (const key of ['_authtoken', '-authtoken']) { 416 if (this.get(key, entryWhere)) { 417 authProblems.push({ action: 'delete', key, where: entryWhere }) 418 } 419 } 420 421 // NOTE we pull registry without restricting to the current 'where' because we want to 422 // suggest scoping things to the registry they would be applied to, which is the default 423 // regardless of where it was defined 424 const nerfedReg = nerfDart(this.get('registry')) 425 // keys that should be nerfed but currently are not 426 for (const key of ['_auth', '_authToken', 'username', '_password']) { 427 if (this.get(key, entryWhere)) { 428 // username and _password must both exist in the same file to be recognized correctly 429 if (key === 'username' && !this.get('_password', entryWhere)) { 430 authProblems.push({ action: 'delete', key, where: entryWhere }) 431 } else if (key === '_password' && !this.get('username', entryWhere)) { 432 authProblems.push({ action: 'delete', key, where: entryWhere }) 433 } else { 434 authProblems.push({ 435 action: 'rename', 436 from: key, 437 to: `${nerfedReg}:${key}`, 438 where: entryWhere, 439 }) 440 } 441 } 442 } 443 } 444 } 445 446 if (authProblems.length) { 447 throw new ErrInvalidAuth(authProblems) 448 } 449 450 return valid 451 } else { 452 const obj = this.data.get(where) 453 obj[_valid] = true 454 455 nopt.invalidHandler = (k, val, type) => 456 this.invalidHandler(k, val, type, obj.source, where) 457 458 nopt.clean(obj.data, this.types, typeDefs) 459 460 nopt.invalidHandler = null 461 return obj[_valid] 462 } 463 } 464 465 // fixes problems identified by validate(), accepts the 'problems' property from a thrown 466 // ErrInvalidAuth to avoid having to check everything again 467 repair (problems) { 468 if (!problems) { 469 try { 470 this.validate() 471 } catch (err) { 472 // coverage skipped here because we don't need to test re-throwing an error 473 // istanbul ignore next 474 if (err.code !== 'ERR_INVALID_AUTH') { 475 throw err 476 } 477 478 problems = err.problems 479 } finally { 480 if (!problems) { 481 problems = [] 482 } 483 } 484 } 485 486 for (const problem of problems) { 487 // coverage disabled for else branch because it doesn't do anything and shouldn't 488 // istanbul ignore else 489 if (problem.action === 'delete') { 490 this.delete(problem.key, problem.where) 491 } else if (problem.action === 'rename') { 492 const raw = this.data.get(problem.where).raw?.[problem.from] 493 const calculated = this.get(problem.from, problem.where) 494 this.set(problem.to, raw || calculated, problem.where) 495 this.delete(problem.from, problem.where) 496 } 497 } 498 } 499 500 // Returns true if the value is coming directly from the source defined 501 // in default definitions, if the current value for the key config is 502 // coming from any other different source, returns false 503 isDefault (key) { 504 const [defaultType, ...types] = [...confTypes] 505 const defaultData = this.data.get(defaultType).data 506 507 return hasOwnProperty(defaultData, key) 508 && types.every(type => { 509 const typeData = this.data.get(type).data 510 return !hasOwnProperty(typeData, key) 511 }) 512 } 513 514 invalidHandler (k, val, type, source, where) { 515 log.warn( 516 'invalid config', 517 k + '=' + JSON.stringify(val), 518 `set in ${source}` 519 ) 520 this.data.get(where)[_valid] = false 521 522 if (Array.isArray(type)) { 523 if (type.includes(typeDefs.url.type)) { 524 type = typeDefs.url.type 525 } else { 526 /* istanbul ignore if - no actual configs matching this, but 527 * path types SHOULD be handled this way, like URLs, for the 528 * same reason */ 529 if (type.includes(typeDefs.path.type)) { 530 type = typeDefs.path.type 531 } 532 } 533 } 534 535 const typeDesc = typeDescription(type) 536 const mustBe = typeDesc 537 .filter(m => m !== undefined && m !== Array) 538 const msg = 'Must be' + this.#getOneOfKeywords(mustBe, typeDesc) 539 const desc = mustBe.length === 1 ? mustBe[0] 540 : [...new Set(mustBe.map(n => typeof n === 'string' ? n : JSON.stringify(n)))].join(', ') 541 log.warn('invalid config', msg, desc) 542 } 543 544 #getOneOfKeywords (mustBe, typeDesc) { 545 let keyword 546 if (mustBe.length === 1 && typeDesc.includes(Array)) { 547 keyword = ' one or more' 548 } else if (mustBe.length > 1 && typeDesc.includes(Array)) { 549 keyword = ' one or more of:' 550 } else if (mustBe.length > 1) { 551 keyword = ' one of:' 552 } else { 553 keyword = '' 554 } 555 return keyword 556 } 557 558 #loadObject (obj, where, source, er = null) { 559 // obj is the raw data read from the file 560 const conf = this.data.get(where) 561 if (conf.source) { 562 const m = `double-loading "${where}" configs from ${source}, ` + 563 `previously loaded from ${conf.source}` 564 throw new Error(m) 565 } 566 567 if (this.sources.has(source)) { 568 const m = `double-loading config "${source}" as "${where}", ` + 569 `previously loaded as "${this.sources.get(source)}"` 570 throw new Error(m) 571 } 572 573 conf.source = source 574 this.sources.set(source, where) 575 if (er) { 576 conf.loadError = er 577 if (er.code !== 'ENOENT') { 578 log.verbose('config', `error loading ${where} config`, er) 579 } 580 } else { 581 conf.raw = obj 582 for (const [key, value] of Object.entries(obj)) { 583 const k = envReplace(key, this.env) 584 const v = this.parseField(value, k) 585 if (where !== 'default') { 586 this.#checkDeprecated(k, where, obj, [key, value]) 587 if (this.definitions[key]?.exclusive) { 588 for (const exclusive of this.definitions[key].exclusive) { 589 if (!this.isDefault(exclusive)) { 590 throw new TypeError(`--${key} can not be provided when using --${exclusive}`) 591 } 592 } 593 } 594 } 595 conf.data[k] = v 596 } 597 } 598 } 599 600 #checkDeprecated (key, where, obj, kv) { 601 // XXX(npm9+) make this throw an error 602 if (this.deprecated[key]) { 603 log.warn('config', key, this.deprecated[key]) 604 } 605 } 606 607 // Parse a field, coercing it to the best type available. 608 parseField (f, key, listElement = false) { 609 return parseField(f, key, this, listElement) 610 } 611 612 async #loadFile (file, type) { 613 process.emit('time', 'config:load:file:' + file) 614 // only catch the error from readFile, not from the loadObject call 615 await readFile(file, 'utf8').then( 616 data => { 617 const parsedConfig = ini.parse(data) 618 if (type === 'project' && parsedConfig.prefix) { 619 // Log error if prefix is mentioned in project .npmrc 620 /* eslint-disable-next-line max-len */ 621 log.error('config', `prefix cannot be changed from project config: ${file}.`) 622 } 623 return this.#loadObject(parsedConfig, type, file) 624 }, 625 er => this.#loadObject(null, type, file, er) 626 ) 627 process.emit('timeEnd', 'config:load:file:' + file) 628 } 629 630 loadBuiltinConfig () { 631 return this.#loadFile(resolve(this.npmPath, 'npmrc'), 'builtin') 632 } 633 634 async loadProjectConfig () { 635 // the localPrefix can be set by the CLI config, but otherwise is 636 // found by walking up the folder tree. either way, we load it before 637 // we return to make sure localPrefix is set 638 await this.loadLocalPrefix() 639 640 // if we have not detected a local package json yet, try now that we 641 // have a local prefix 642 if (this.localPackage == null) { 643 this.localPackage = await fileExists(this.localPrefix, 'package.json') 644 } 645 646 if (this.#get('global') === true || this.#get('location') === 'global') { 647 this.data.get('project').source = '(global mode enabled, ignored)' 648 this.sources.set(this.data.get('project').source, 'project') 649 return 650 } 651 652 const projectFile = resolve(this.localPrefix, '.npmrc') 653 // if we're in the ~ directory, and there happens to be a node_modules 654 // folder (which is not TOO uncommon, it turns out), then we can end 655 // up loading the "project" config where the "userconfig" will be, 656 // which causes some calamaties. So, we only load project config if 657 // it doesn't match what the userconfig will be. 658 if (projectFile !== this.#get('userconfig')) { 659 return this.#loadFile(projectFile, 'project') 660 } else { 661 this.data.get('project').source = '(same as "user" config, ignored)' 662 this.sources.set(this.data.get('project').source, 'project') 663 } 664 } 665 666 async loadLocalPrefix () { 667 const cliPrefix = this.#get('prefix', 'cli') 668 if (cliPrefix) { 669 this.localPrefix = cliPrefix 670 return 671 } 672 673 const cliWorkspaces = this.#get('workspaces', 'cli') 674 const isGlobal = this.#get('global') || this.#get('location') === 'global' 675 676 for (const p of walkUp(this.cwd)) { 677 // HACK: this is an option set in tests to stop the local prefix from being set 678 // on tests that are created inside the npm repo 679 if (this.excludeNpmCwd && p === this.npmPath) { 680 break 681 } 682 683 const hasPackageJson = await fileExists(p, 'package.json') 684 685 if (!this.localPrefix && (hasPackageJson || await dirExists(p, 'node_modules'))) { 686 this.localPrefix = p 687 this.localPackage = hasPackageJson 688 689 // if workspaces are disabled, or we're in global mode, return now 690 if (cliWorkspaces === false || isGlobal) { 691 return 692 } 693 694 // otherwise, continue the loop 695 continue 696 } 697 698 if (this.localPrefix && hasPackageJson) { 699 // if we already set localPrefix but this dir has a package.json 700 // then we need to see if `p` is a workspace root by reading its package.json 701 // however, if reading it fails then we should just move on 702 const pkg = await rpj(resolve(p, 'package.json')).catch(() => false) 703 if (!pkg) { 704 continue 705 } 706 707 const workspaces = await mapWorkspaces({ cwd: p, pkg }) 708 for (const w of workspaces.values()) { 709 if (w === this.localPrefix) { 710 // see if there's a .npmrc file in the workspace, if so log a warning 711 if (await fileExists(this.localPrefix, '.npmrc')) { 712 log.warn(`ignoring workspace config at ${this.localPrefix}/.npmrc`) 713 } 714 715 // set the workspace in the default layer, which allows it to be overridden easily 716 const { data } = this.data.get('default') 717 data.workspace = [this.localPrefix] 718 this.localPrefix = p 719 this.localPackage = hasPackageJson 720 log.info(`found workspace root at ${this.localPrefix}`) 721 // we found a root, so we return now 722 return 723 } 724 } 725 } 726 } 727 728 if (!this.localPrefix) { 729 this.localPrefix = this.cwd 730 } 731 } 732 733 loadUserConfig () { 734 return this.#loadFile(this.#get('userconfig'), 'user') 735 } 736 737 loadGlobalConfig () { 738 return this.#loadFile(this.#get('globalconfig'), 'global') 739 } 740 741 async save (where) { 742 if (!this.loaded) { 743 throw new Error('call config.load() before saving') 744 } 745 if (!confFileTypes.has(where)) { 746 throw new Error('invalid config location param: ' + where) 747 } 748 749 const conf = this.data.get(where) 750 conf[_loadError] = null 751 752 if (where === 'user') { 753 // if email is nerfed, then we want to de-nerf it 754 const nerfed = nerfDart(this.get('registry')) 755 const email = this.get(`${nerfed}:email`, 'user') 756 if (email) { 757 this.delete(`${nerfed}:email`, 'user') 758 this.set('email', email, 'user') 759 } 760 } 761 762 // We need the actual raw data before we called parseField so that we are 763 // saving the same content back to the file 764 const iniData = ini.stringify(conf.raw).trim() + '\n' 765 if (!iniData.trim()) { 766 // ignore the unlink error (eg, if file doesn't exist) 767 await unlink(conf.source).catch(er => {}) 768 return 769 } 770 const dir = dirname(conf.source) 771 await mkdir(dir, { recursive: true }) 772 await writeFile(conf.source, iniData, 'utf8') 773 const mode = where === 'user' ? 0o600 : 0o666 774 await chmod(conf.source, mode) 775 } 776 777 clearCredentialsByURI (uri, level = 'user') { 778 const nerfed = nerfDart(uri) 779 const def = nerfDart(this.get('registry')) 780 if (def === nerfed) { 781 this.delete(`-authtoken`, level) 782 this.delete(`_authToken`, level) 783 this.delete(`_authtoken`, level) 784 this.delete(`_auth`, level) 785 this.delete(`_password`, level) 786 this.delete(`username`, level) 787 // de-nerf email if it's nerfed to the default registry 788 const email = this.get(`${nerfed}:email`, level) 789 if (email) { 790 this.set('email', email, level) 791 } 792 } 793 this.delete(`${nerfed}:_authToken`, level) 794 this.delete(`${nerfed}:_auth`, level) 795 this.delete(`${nerfed}:_password`, level) 796 this.delete(`${nerfed}:username`, level) 797 this.delete(`${nerfed}:email`, level) 798 this.delete(`${nerfed}:certfile`, level) 799 this.delete(`${nerfed}:keyfile`, level) 800 } 801 802 setCredentialsByURI (uri, { token, username, password, email, certfile, keyfile }) { 803 const nerfed = nerfDart(uri) 804 805 // email is either provided, a top level key, or nothing 806 email = email || this.get('email', 'user') 807 808 // field that hasn't been used as documented for a LONG time, 809 // and as of npm 7.10.0, isn't used at all. We just always 810 // send auth if we have it, only to the URIs under the nerf dart. 811 this.delete(`${nerfed}:always-auth`, 'user') 812 813 this.delete(`${nerfed}:email`, 'user') 814 if (certfile && keyfile) { 815 this.set(`${nerfed}:certfile`, certfile, 'user') 816 this.set(`${nerfed}:keyfile`, keyfile, 'user') 817 // cert/key may be used in conjunction with other credentials, thus no `else` 818 } 819 if (token) { 820 this.set(`${nerfed}:_authToken`, token, 'user') 821 this.delete(`${nerfed}:_password`, 'user') 822 this.delete(`${nerfed}:username`, 'user') 823 } else if (username || password) { 824 if (!username) { 825 throw new Error('must include username') 826 } 827 if (!password) { 828 throw new Error('must include password') 829 } 830 this.delete(`${nerfed}:_authToken`, 'user') 831 this.set(`${nerfed}:username`, username, 'user') 832 // note: not encrypted, no idea why we bothered to do this, but oh well 833 // protects against shoulder-hacks if password is memorable, I guess? 834 const encoded = Buffer.from(password, 'utf8').toString('base64') 835 this.set(`${nerfed}:_password`, encoded, 'user') 836 } else if (!certfile || !keyfile) { 837 throw new Error('No credentials to set.') 838 } 839 } 840 841 // this has to be a bit more complicated to support legacy data of all forms 842 getCredentialsByURI (uri) { 843 const nerfed = nerfDart(uri) 844 const def = nerfDart(this.get('registry')) 845 const creds = {} 846 847 // email is handled differently, it used to always be nerfed and now it never should be 848 // if it's set nerfed to the default registry, then we copy it to the unnerfed key 849 // TODO: evaluate removing 'email' from the credentials object returned here 850 const email = this.get(`${nerfed}:email`) || this.get('email') 851 if (email) { 852 if (nerfed === def) { 853 this.set('email', email, 'user') 854 } 855 creds.email = email 856 } 857 858 const certfileReg = this.get(`${nerfed}:certfile`) 859 const keyfileReg = this.get(`${nerfed}:keyfile`) 860 if (certfileReg && keyfileReg) { 861 creds.certfile = certfileReg 862 creds.keyfile = keyfileReg 863 // cert/key may be used in conjunction with other credentials, thus no `return` 864 } 865 866 const tokenReg = this.get(`${nerfed}:_authToken`) 867 if (tokenReg) { 868 creds.token = tokenReg 869 return creds 870 } 871 872 const userReg = this.get(`${nerfed}:username`) 873 const passReg = this.get(`${nerfed}:_password`) 874 if (userReg && passReg) { 875 creds.username = userReg 876 creds.password = Buffer.from(passReg, 'base64').toString('utf8') 877 const auth = `${creds.username}:${creds.password}` 878 creds.auth = Buffer.from(auth, 'utf8').toString('base64') 879 return creds 880 } 881 882 const authReg = this.get(`${nerfed}:_auth`) 883 if (authReg) { 884 const authDecode = Buffer.from(authReg, 'base64').toString('utf8') 885 const authSplit = authDecode.split(':') 886 creds.username = authSplit.shift() 887 creds.password = authSplit.join(':') 888 creds.auth = authReg 889 return creds 890 } 891 892 // at this point, nothing else is usable so just return what we do have 893 return creds 894 } 895 896 // set up the environment object we have with npm_config_* environs 897 // for all configs that are different from their default values, and 898 // set EDITOR and HOME. 899 setEnvs () { 900 setEnvs(this) 901 } 902} 903 904const _loadError = Symbol('loadError') 905const _valid = Symbol('valid') 906 907class ConfigData { 908 #data 909 #source = null 910 #raw = null 911 constructor (parent) { 912 this.#data = Object.create(parent && parent.data) 913 this.#raw = {} 914 this[_valid] = true 915 } 916 917 get data () { 918 return this.#data 919 } 920 921 get valid () { 922 return this[_valid] 923 } 924 925 set source (s) { 926 if (this.#source) { 927 throw new Error('cannot set ConfigData source more than once') 928 } 929 this.#source = s 930 } 931 932 get source () { 933 return this.#source 934 } 935 936 set loadError (e) { 937 if (this[_loadError] || (Object.keys(this.#raw).length)) { 938 throw new Error('cannot set ConfigData loadError after load') 939 } 940 this[_loadError] = e 941 } 942 943 get loadError () { 944 return this[_loadError] 945 } 946 947 set raw (r) { 948 if (Object.keys(this.#raw).length || this[_loadError]) { 949 throw new Error('cannot set ConfigData raw after load') 950 } 951 this.#raw = r 952 } 953 954 get raw () { 955 return this.#raw 956 } 957} 958 959module.exports = Config 960