JFIF  ` ` ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 85 C   !"$"$ C hh"       } !1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz      w !1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz   ? .(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((+QI3L( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( @y -#yIޚXP2$袊 ( ( ( ( ( ( ( ( ( ( ( ( ( ( 3FE a(dN^M4GӁMoO$dssYz4]4+HM9վ2#OL@ܟH O@}LYW=p3! 7'Z&;" zםg|Ij~DǶr^2sI🆒TS(2?y}xmux#sھ[Cqg0mr\ ҏĺҋ{d^Rrr#mOŬ]\>uKcq*?ɯL񆥥Ƕ[ĆlO b[2+'c9Yl{I`:R fڽ;g8jc}ZC)ʑX匌J@:(P Hzh@#֌6Y$ʹ -ڛhJۆqi7s^{|c)]zyۿP+𗈬MaH_?LfG/U ( ( ( ( ( ( ( ("iX1?.ቨ.a?6I"|O+<.2H]aizhV3G8v|t".RcR7t+d(E~0 zD$r2ۜ~oC+Qsvx~%8 ~_|@Nկ81\gNMI#x ÞƠ W Cidlu;$1G5xh\NM#oL Zv6u+fd$HbaKiܻN?ʥt0F۟+g ]Q+r{8"28S4.ofXɑ|֗ x&s_Мp{W.14h~RME;XVX`q֡|:֢oB1fSBP_ WI~Ծ<04%ar UΉ>cy ~1b5w|Iϊ1ٯ4D`A y{-+N SkڲVCXR)M^ѠSc^3! }b}VBَ'9ִ/j~!]/DY_'׿} UQDRBͲ>GLsSnJQ(W Cx`rbF }G:~çivZ1Q =V&,gߥI}kdsѻ4RVEPEPEPEPEPEPH)I1@ (;=7JkO8j_ ٛVr\Q0 "=_Z;-8*ᛯ[ino ZU>#/9]x0]> 3u&%S9/PQVҭtIbxP ==*9 jPX7uMr3<`Z\g:^qV \WKg.잹ȩZ]][Pi۬h=V'O2VK0 TI`z'_֚CH/G Է4V 5, up쇖?夿w5j9]APn<:cs[V57 ]vAl*բp֖Z Pu{ _N]6@ȫaՏ@6sX+W1q@ tX 鴫`I8h3D Gm vCċiҝzRzP!Ҕ`p:P }>5|4VR!-䝒I,O98(=e"|ۦuK *pl?B k@u=$ Mz%%Sr~SsW۴ͧ1;Lg99qw:)Ծ~Ǟ+|7s]Md 9)V5"ھ;/?p MLGN=ҾÈ67c&XL1#}0瞾o};^̠yJ}}ǡǾ%!<zbfEe}I]ֽ NzZ-T0I WiiehRin-lPN׉bgfIìh% $L vˆ*n.Kr{waҷRnGz<+d63G <M4Ҽ' m4H ǪՅ$qpxZZ)gڊ(((((( W|sZ|akmD:rJELr~wkMCX _v4E|!e u.W%1zu?θߎ b%#QB#ApN9^78R ᭭DdA&ӶJ:h9>ct`lҝQ+9 erGn"  0=*(((((&4u+`=cpoōj:-pwp~Q*%*nH/=6a#4׎cÚwyC޾W})ǚEEQ4w_JȨÔjf5>Ϟ[<t(c ~m1K#SI1S@ Z@5F)E0 ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( )8;qzj6:s3yv ӥ b|J ͭj'*"㔃Ty ėc^lb=$7 N4a. R#!{\ts '᳣xj ao% T9ڔ4|QwZ|Z20k G>xO^;]o..tn7h ֆ[\y5/glKv:xu^)-_ۏLwOY5y` 6gҴ/ɬi[X~77O_ x>}NU =?=6;XDa+XSHRlA Q<7ֺ8@ԊzSA7qB Z(AEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP^WZ#GkM-] eRH-##0'TH( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ^cef6b88lr1ڀ< &[^dB>ץYYh#0JpP c4  QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE QE 'use strict' const BB = require('bluebird') const ansistyles = require('ansistyles') const figgyPudding = require('figgy-pudding') const inspect = require('util').inspect const log = require('npmlog') const npm = require('./npm.js') const npmConfig = require('./config/figgy-config.js') const otplease = require('./utils/otplease.js') const output = require('./utils/output.js') const profile = require('libnpm/profile') const pulseTillDone = require('./utils/pulse-till-done.js') const qrcodeTerminal = require('qrcode-terminal') const queryString = require('query-string') const qw = require('qw') const readUserInfo = require('./utils/read-user-info.js') const Table = require('cli-table3') const url = require('url') module.exports = profileCmd profileCmd.usage = 'npm profile enable-2fa [auth-only|auth-and-writes]\n' + 'npm profile disable-2fa\n' + 'npm profile get []\n' + 'npm profile set ' profileCmd.subcommands = qw`enable-2fa disable-2fa get set` profileCmd.completion = function (opts, cb) { var argv = opts.conf.argv.remain switch (argv[2]) { case 'enable-2fa': case 'enable-tfa': if (argv.length === 3) { return cb(null, qw`auth-and-writes auth-only`) } else { return cb(null, []) } case 'disable-2fa': case 'disable-tfa': case 'get': case 'set': return cb(null, []) default: return cb(new Error(argv[2] + ' not recognized')) } } function withCb (prom, cb) { prom.then((value) => cb(null, value), cb) } const ProfileOpts = figgyPudding({ json: {}, otp: {}, parseable: {}, registry: {} }) function profileCmd (args, cb) { if (args.length === 0) return cb(new Error(profileCmd.usage)) log.gauge.show('profile') switch (args[0]) { case 'enable-2fa': case 'enable-tfa': case 'enable2fa': case 'enabletfa': withCb(enable2fa(args.slice(1)), cb) break case 'disable-2fa': case 'disable-tfa': case 'disable2fa': case 'disabletfa': withCb(disable2fa(), cb) break case 'get': withCb(get(args.slice(1)), cb) break case 'set': withCb(set(args.slice(1)), cb) break default: cb(new Error('Unknown profile command: ' + args[0])) } } const knownProfileKeys = qw` name email ${'two-factor auth'} fullname homepage freenode twitter github created updated` function get (args) { const tfa = 'two-factor auth' const conf = ProfileOpts(npmConfig()) return pulseTillDone.withPromise(profile.get(conf)).then((info) => { if (!info.cidr_whitelist) delete info.cidr_whitelist if (conf.json) { output(JSON.stringify(info, null, 2)) return } const cleaned = {} knownProfileKeys.forEach((k) => { cleaned[k] = info[k] || '' }) Object.keys(info).filter((k) => !(k in cleaned)).forEach((k) => { cleaned[k] = info[k] || '' }) delete cleaned.tfa delete cleaned.email_verified cleaned['email'] += info.email_verified ? ' (verified)' : '(unverified)' if (info.tfa && !info.tfa.pending) { cleaned[tfa] = info.tfa.mode } else { cleaned[tfa] = 'disabled' } if (args.length) { const values = args // comma or space separated ↓ .join(',').split(/,/).map((arg) => arg.trim()).filter((arg) => arg !== '') .map((arg) => cleaned[arg]) .join('\t') output(values) } else { if (conf.parseable) { Object.keys(info).forEach((key) => { if (key === 'tfa') { output(`${key}\t${cleaned[tfa]}`) } else { output(`${key}\t${info[key]}`) } }) } else { const table = new Table() Object.keys(cleaned).forEach((k) => table.push({[ansistyles.bright(k)]: cleaned[k]})) output(table.toString()) } } }) } const writableProfileKeys = qw` email password fullname homepage freenode twitter github` function set (args) { let conf = ProfileOpts(npmConfig()) const prop = (args[0] || '').toLowerCase().trim() let value = args.length > 1 ? args.slice(1).join(' ') : null if (prop !== 'password' && value === null) { return Promise.reject(Error('npm profile set ')) } if (prop === 'password' && value !== null) { return Promise.reject(Error( 'npm profile set password\n' + 'Do not include your current or new passwords on the command line.')) } if (writableProfileKeys.indexOf(prop) === -1) { return Promise.reject(Error(`"${prop}" is not a property we can set. Valid properties are: ` + writableProfileKeys.join(', '))) } return BB.try(() => { if (prop === 'password') { return readUserInfo.password('Current password: ').then((current) => { return readPasswords().then((newpassword) => { value = {old: current, new: newpassword} }) }) } else if (prop === 'email') { return readUserInfo.password('Password: ').then((current) => { return {password: current, email: value} }) } function readPasswords () { return readUserInfo.password('New password: ').then((password1) => { return readUserInfo.password(' Again: ').then((password2) => { if (password1 !== password2) { log.warn('profile', 'Passwords do not match, please try again.') return readPasswords() } return password1 }) }) } }).then(() => { // FIXME: Work around to not clear everything other than what we're setting return pulseTillDone.withPromise(profile.get(conf).then((user) => { const newUser = {} writableProfileKeys.forEach((k) => { newUser[k] = user[k] }) newUser[prop] = value return otplease(conf, conf => profile.set(newUser, conf)) .then((result) => { if (conf.json) { output(JSON.stringify({[prop]: result[prop]}, null, 2)) } else if (conf.parseable) { output(prop + '\t' + result[prop]) } else if (result[prop] != null) { output('Set', prop, 'to', result[prop]) } else { output('Set', prop) } }) })) }) } function enable2fa (args) { if (args.length > 1) { return Promise.reject(new Error('npm profile enable-2fa [auth-and-writes|auth-only]')) } const mode = args[0] || 'auth-and-writes' if (mode !== 'auth-only' && mode !== 'auth-and-writes') { return Promise.reject(new Error(`Invalid two-factor authentication mode "${mode}".\n` + 'Valid modes are:\n' + ' auth-only - Require two-factor authentication only when logging in\n' + ' auth-and-writes - Require two-factor authentication when logging in AND when publishing')) } const conf = ProfileOpts(npmConfig()) if (conf.json || conf.parseable) { return Promise.reject(new Error( 'Enabling two-factor authentication is an interactive operation and ' + (conf.json ? 'JSON' : 'parseable') + ' output mode is not available')) } const info = { tfa: { mode: mode } } return BB.try(() => { // if they're using legacy auth currently then we have to update them to a // bearer token before continuing. const auth = getAuth(conf) if (auth.basic) { log.info('profile', 'Updating authentication to bearer token') return profile.createToken( auth.basic.password, false, [], conf ).then((result) => { if (!result.token) throw new Error('Your registry ' + conf.registry + 'does not seem to support bearer tokens. Bearer tokens are required for two-factor authentication') npm.config.setCredentialsByURI(conf.registry, {token: result.token}) return BB.fromNode((cb) => npm.config.save('user', cb)) }) } }).then(() => { log.notice('profile', 'Enabling two factor authentication for ' + mode) return readUserInfo.password() }).then((password) => { info.tfa.password = password log.info('profile', 'Determine if tfa is pending') return pulseTillDone.withPromise(profile.get(conf)).then((info) => { if (!info.tfa) return if (info.tfa.pending) { log.info('profile', 'Resetting two-factor authentication') return pulseTillDone.withPromise(profile.set({tfa: {password, mode: 'disable'}}, conf)) } else { if (conf.auth.otp) return return readUserInfo.otp('Enter one-time password from your authenticator app: ').then((otp) => { conf.auth.otp = otp }) } }) }).then(() => { log.info('profile', 'Setting two-factor authentication to ' + mode) return pulseTillDone.withPromise(profile.set(info, conf)) }).then((challenge) => { if (challenge.tfa === null) { output('Two factor authentication mode changed to: ' + mode) return } if (typeof challenge.tfa !== 'string' || !/^otpauth:[/][/]/.test(challenge.tfa)) { throw new Error('Unknown error enabling two-factor authentication. Expected otpauth URL, got: ' + inspect(challenge.tfa)) } const otpauth = url.parse(challenge.tfa) const opts = queryString.parse(otpauth.query) return qrcode(challenge.tfa).then((code) => { output('Scan into your authenticator app:\n' + code + '\n Or enter code:', opts.secret) }).then((code) => { return readUserInfo.otp('And an OTP code from your authenticator: ') }).then((otp1) => { log.info('profile', 'Finalizing two-factor authentication') return profile.set({tfa: [otp1]}, conf) }).then((result) => { output('2FA successfully enabled. Below are your recovery codes, please print these out.') output('You will need these to recover access to your account if you lose your authentication device.') result.tfa.forEach((c) => output('\t' + c)) }) }) } function getAuth (conf) { const creds = npm.config.getCredentialsByURI(conf.registry) let auth if (creds.token) { auth = {token: creds.token} } else if (creds.username) { auth = {basic: {username: creds.username, password: creds.password}} } else if (creds.auth) { const basic = Buffer.from(creds.auth, 'base64').toString().split(':', 2) auth = {basic: {username: basic[0], password: basic[1]}} } else { auth = {} } if (conf.otp) auth.otp = conf.otp return auth } function disable2fa (args) { let conf = ProfileOpts(npmConfig()) return pulseTillDone.withPromise(profile.get(conf)).then((info) => { if (!info.tfa || info.tfa.pending) { output('Two factor authentication not enabled.') return } return readUserInfo.password().then((password) => { return BB.try(() => { if (conf.otp) return return readUserInfo.otp('Enter one-time password from your authenticator: ').then((otp) => { conf = conf.concat({otp}) }) }).then(() => { log.info('profile', 'disabling tfa') return pulseTillDone.withPromise(profile.set({tfa: {password: password, mode: 'disable'}}, conf)).then(() => { if (conf.json) { output(JSON.stringify({tfa: false}, null, 2)) } else if (conf.parseable) { output('tfa\tfalse') } else { output('Two factor authentication disabled.') } }) }) }) }) } function qrcode (url) { return new Promise((resolve) => qrcodeTerminal.generate(url, resolve)) }