Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/npm/cli.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRebecca Turner <me@re-becca.org>2017-09-28 21:30:00 +0300
committerRebecca Turner <me@re-becca.org>2017-10-04 11:08:25 +0300
commitf6ebf5e8bd6a212c7661e248c62c423f2b54d978 (patch)
tree438579b0b8bec9935866a42f3b73a05ed79be086
parent6d2a285a58655f10834f64d38449eb1f3c8b6c47 (diff)
profile,token: Add new profile commands
-rw-r--r--doc/cli/npm-profile.md74
-rw-r--r--doc/cli/npm-token.md59
-rw-r--r--doc/misc/npm-config.md22
-rw-r--r--lib/config/cmd-list.js2
-rw-r--r--lib/config/defaults.js8
-rw-r--r--lib/profile.js296
-rw-r--r--lib/token.js211
-rw-r--r--lib/utils/pulse-till-done.js40
-rw-r--r--lib/utils/read-user-info.js66
9 files changed, 766 insertions, 12 deletions
diff --git a/doc/cli/npm-profile.md b/doc/cli/npm-profile.md
new file mode 100644
index 000000000..16b5e11b6
--- /dev/null
+++ b/doc/cli/npm-profile.md
@@ -0,0 +1,74 @@
+npm-profile(1) -- Change settings on your registry profile
+==========================================================
+
+## SYNOPSIS
+
+ npm profile get [--json|--parseable] [<property>]
+ npm profile set [--json|--parseable] <property> <value>
+ npm profile set password
+ npm profile enable-2fa [auth-and-writes|auth-only]
+ npm profile disable-2fa
+
+## DESCRIPTION
+
+Change your profile information on the registry. This not be available if
+you're using a non-npmjs registry.
+
+* `npm profile get [<property>]`:
+ Display all of the properties of your profile, or one or more specific
+ properties. It looks like:
+
+```
++-----------------+---------------------------+
+| name | example |
++-----------------+---------------------------+
+| email | me@example.com (verified) |
++-----------------+---------------------------+
+| two factor auth | auth-and-writes |
++-----------------+---------------------------+
+| fullname | Example User |
++-----------------+---------------------------+
+| homepage | |
++-----------------+---------------------------+
+| freenode | |
++-----------------+---------------------------+
+| twitter | |
++-----------------+---------------------------+
+| github | |
++-----------------+---------------------------+
+| created | 2015-02-26T01:38:35.892Z |
++-----------------+---------------------------+
+| updated | 2017-10-02T21:29:45.922Z |
++-----------------+---------------------------+
+```
+
+* `npm profile set <property> <value>`:
+ Set the value of a profile property. You can set the following properties this way:
+ email, fullname, homepage, freenode, twitter, github
+
+* `npm profile set password`:
+ Change your password. This is interactive, you'll be prompted for your
+ current password and a new password. You'll also be prompted for an OTP
+ if you have two-factor authentication enabled.
+
+* `npm profile enable-2fa [auth-and-writes|auth-only]`:
+ Enables two-factor authentication. Defaults to `auth-and-writes` mode. Modes are:
+ * `auth-only`: Require an OTP when logging in or making changes to your
+ account's authentication. The OTP will be required on both the website
+ and the command line.
+ * `auth-and-writes`: Requires an OTP at all the times `auth-only` does, and also requires one when
+ publishing a module, setting the `latest` dist-tag, or changing access
+ via `npm access` and `npm owner`.
+
+* `npm profile disable-2fa`:
+ Disables two-factor authentication.
+
+## DETAILS
+
+All of the `npm profile` subcommands accept `--json` and `--parseable` and
+will tailor their output based on those. Some of these commands may not be
+available on non npmjs.com registries.
+
+## SEE ALSO
+
+* npm-config(7)
diff --git a/doc/cli/npm-token.md b/doc/cli/npm-token.md
new file mode 100644
index 000000000..dbc7ea9ee
--- /dev/null
+++ b/doc/cli/npm-token.md
@@ -0,0 +1,59 @@
+npm-token(1) -- Manage your authentication tokens
+=================================================
+
+## SYNOPSIS
+
+ npm token list [--json|--parseable]
+ npm token create [--read-only] [--cidr=1.1.1.1/24,2.2.2.2/16]
+ npm token delete <id|token>
+
+## DESCRIPTION
+
+This list you list, create and delete authentication tokens.
+
+* `npm token list`:
+ Shows a table of all active authentication tokens. You can request this as
+ JSON with `--json` or tab-separated values with `--parseable`.
+```
++--------+---------+------------+----------+----------------+
+| id | token | created | read-only | CIDR whitelist |
++--------+---------+------------+----------+----------------+
+| 7f3134 | 1fa9ba… | 2017-10-02 | yes | |
++--------+---------+------------+----------+----------------+
+| c03241 | af7aef… | 2017-10-02 | no | 192.168.0.1/24 |
++--------+---------+------------+----------+----------------+
+| e0cf92 | 3a436a… | 2017-10-02 | no | |
++--------+---------+------------+----------+----------------+
+| 63eb9d | 74ef35… | 2017-09-28 | no | |
++--------+---------+------------+----------+----------------+
+| 2daaa8 | cbad5f… | 2017-09-26 | no | |
++--------+---------+------------+----------+----------------+
+| 68c2fe | 127e51… | 2017-09-23 | no | |
++--------+---------+------------+----------+----------------+
+| 6334e1 | 1dadd1… | 2017-09-23 | no | |
++--------+---------+------------+----------+----------------+
+```
+
+* `npm token create [--read-only] [--cidr=<cidr-ranges>]`:
+ Create a new authentication token. It can be `--read-only` or accept a list of
+ [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) ranges to
+ limit use of this token to. This will prompt you for your password, and, if you have
+ two-factor authentication enabled, an otp.
+
+```
++----------------+--------------------------------------+
+| token | a73c9572-f1b9-8983-983d-ba3ac3cc913d |
++----------------+--------------------------------------+
+| cidr_whitelist | |
++----------------+--------------------------------------+
+| readonly | false |
++----------------+--------------------------------------+
+| created | 2017-10-02T07:52:24.838Z |
++----------------+--------------------------------------+
+```
+
+* `npm token delete <token|id>`:
+ This removes an authentication token, making it immediately unusable. This can accept
+ both complete tokens (as you get back from `npm token create` and will
+ find in your `.npmrc`) and ids as seen in the `npm token list` output.
+ This will NOT accept the truncated token found in `npm token list` output.
diff --git a/doc/misc/npm-config.md b/doc/misc/npm-config.md
index 30ea578f0..3c9ec3881 100644
--- a/doc/misc/npm-config.md
+++ b/doc/misc/npm-config.md
@@ -269,6 +269,13 @@ PEM format (Windows calls it "Base-64 encoded X.509 (.CER)") with newlines repla
It is _not_ the path to a certificate file (and there is no "certfile" option).
+### cidr
+
+* Default: `null`
+* Type: String, Array, null
+
+This is a list of CIDR address to be used when configuring limited access tokens with the `npm token create` command.
+
### color
* Default: true
@@ -699,6 +706,14 @@ Attempt to install packages in the `optionalDependencies` object. Note
that if these packages fail to install, the overall installation
process is not aborted.
+### otp
+
+* Default: null
+* Type: Number
+
+This is a one-time password from a two-factor authenticator. It's needed
+when publishing or changing package permissions with `npm access`.
+
### package-lock
* Default: true
@@ -773,6 +788,13 @@ A proxy to use for outgoing http requests. If the `HTTP_PROXY` or
`http_proxy` environment variables are set, proxy settings will be
honored by the underlying `request` library.
+### read-only
+
+* Default: false
+* Type: Boolean
+
+This is used to mark a token as unable to publish when configuring limited access tokens with the `npm token create` command.
+
### rebuild-bundle
* Default: true
diff --git a/lib/config/cmd-list.js b/lib/config/cmd-list.js
index f2d5fab17..49c445a4f 100644
--- a/lib/config/cmd-list.js
+++ b/lib/config/cmd-list.js
@@ -74,6 +74,8 @@ var cmdList = [
'team',
'deprecate',
'shrinkwrap',
+ 'token',
+ 'profile',
'help',
'help-search',
diff --git a/lib/config/defaults.js b/lib/config/defaults.js
index 3a566ee0f..35617fd63 100644
--- a/lib/config/defaults.js
+++ b/lib/config/defaults.js
@@ -128,6 +128,8 @@ Object.defineProperty(exports, 'defaults', {get: function () {
cert: null,
+ cidr: null,
+
color: true,
depth: Infinity,
description: true,
@@ -179,6 +181,7 @@ Object.defineProperty(exports, 'defaults', {get: function () {
'onload-script': false,
only: null,
optional: true,
+ otp: null,
'package-lock': true,
parseable: false,
'prefer-offline': false,
@@ -192,6 +195,7 @@ Object.defineProperty(exports, 'defaults', {get: function () {
'node/{node-version} ' +
'{platform} ' +
'{arch}',
+ 'read-only': false,
'rebuild-bundle': true,
registry: 'https://registry.npmjs.org/',
rollback: true,
@@ -257,6 +261,7 @@ exports.types = {
'cache-max': Number,
'cache-min': Number,
cert: [null, String],
+ cidr: [null, String, Array],
color: ['always', Boolean],
depth: Number,
description: Boolean,
@@ -309,6 +314,7 @@ exports.types = {
only: [null, 'dev', 'development', 'prod', 'production'],
optional: Boolean,
'package-lock': Boolean,
+ otp: Number,
parseable: Boolean,
'prefer-offline': Boolean,
'prefer-online': Boolean,
@@ -316,6 +322,7 @@ exports.types = {
production: Boolean,
progress: Boolean,
proxy: [null, false, url], // allow proxy to be disabled explicitly
+ 'read-only': Boolean,
'rebuild-bundle': Boolean,
registry: [null, url],
rollback: Boolean,
@@ -405,6 +412,7 @@ exports.shorthands = {
m: ['--message'],
p: ['--parseable'],
porcelain: ['--parseable'],
+ readonly: ['--read-only'],
g: ['--global'],
S: ['--save'],
D: ['--save-dev'],
diff --git a/lib/profile.js b/lib/profile.js
new file mode 100644
index 000000000..4238e1427
--- /dev/null
+++ b/lib/profile.js
@@ -0,0 +1,296 @@
+'use strict'
+const profile = require('npm-profile')
+const npm = require('./npm.js')
+const log = require('npmlog')
+const output = require('./utils/output.js')
+const qw = require('qw')
+const Table = require('cli-table2')
+const ansistyles = require('ansistyles')
+const Bluebird = require('bluebird')
+const readUserInfo = require('./utils/read-user-info.js')
+const qrcodeTerminal = require('qrcode-terminal')
+const url = require('url')
+const queryString = require('query-string')
+const pulseTillDone = require('./utils/pulse-till-done.js')
+
+module.exports = profileCmd
+
+profileCmd.usage =
+ 'npm profile enable-2fa [auth-only|auth-and-writes]\n' +
+ 'npm profile disable-2fa\n' +
+ 'npm profile get [<key>]\n' +
+ 'npm profile set <key> <value>'
+
+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)
+}
+
+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]))
+ }
+}
+
+function config () {
+ const conf = {
+ json: npm.config.get('json'),
+ parseable: npm.config.get('parseable'),
+ registry: npm.config.get('registry'),
+ otp: npm.config.get('otp')
+ }
+ conf.auth = npm.config.getCredentialsByURI(conf.registry)
+ if (conf.otp) conf.auth.otp = conf.otp
+ return conf
+}
+
+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 = config()
+ 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]}`)
+ }
+ })
+ return
+ } 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) {
+ const conf = config()
+ 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 <prop> <value>'))
+ }
+ 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 Bluebird.try(() => {
+ if (prop !== 'password') return
+ return readUserInfo.password('Current password: ').then((current) => {
+ return readPasswords().then((newpassword) => {
+ value = {old: current, new: newpassword}
+ })
+ })
+ 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 profile.set(newUser, conf).catch((err) => {
+ if (err.code !== 'EOTP') throw err
+ return readUserInfo.otp('Enter OTP: ').then((otp) => {
+ conf.auth.otp = otp
+ return 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 {
+ output('Set', prop, 'to', result[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 = config()
+ if (conf.json || conf.parseable) {
+ return Promise.reject(new Error(
+ 'Enabling two-factor authentication is an interactive opperation and ' +
+ (conf.json ? 'JSON' : 'parseable') + 'output mode is not available'))
+ }
+ log.notice('profile', 'Enabling two factor authentication for ' + mode)
+ const info = {
+ tfa: {
+ mode: 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 OTP: ').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: ' + 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('TFA 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 disable2fa (args) {
+ const conf = config()
+ 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 Bluebird.try(() => {
+ if (conf.auth.otp) return
+ return readUserInfo.otp('Enter one-time password from your authenticator: ').then((otp) => {
+ conf.auth.otp = 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))
+}
diff --git a/lib/token.js b/lib/token.js
new file mode 100644
index 000000000..a182b633d
--- /dev/null
+++ b/lib/token.js
@@ -0,0 +1,211 @@
+'use strict'
+const profile = require('npm-profile')
+const npm = require('./npm.js')
+const output = require('./utils/output.js')
+const Table = require('cli-table2')
+const Bluebird = require('bluebird')
+const isCidrV4 = require('is-cidr').isCidrV4
+const isCidrV6 = require('is-cidr').isCidrV6
+const readUserInfo = require('./utils/read-user-info.js')
+const ansistyles = require('ansistyles')
+const log = require('npmlog')
+const pulseTillDone = require('./utils/pulse-till-done.js')
+
+module.exports = token
+
+token.usage =
+ 'npm token list\n' +
+ 'npm token delete <tokenKey>\n' +
+ 'npm token create [--read-only] [--cidr=list]\n'
+
+token.subcommands = ['list', 'delete', 'create']
+
+token.completion = function (opts, cb) {
+ var argv = opts.conf.argv.remain
+
+ switch (argv[2]) {
+ case 'list':
+ case 'delete':
+ case 'create':
+ return cb(null, [])
+ default:
+ return cb(new Error(argv[2] + ' not recognized'))
+ }
+}
+
+function withCb (prom, cb) {
+ prom.then((value) => cb(null, value), cb)
+}
+
+function token (args, cb) {
+ log.gauge.show('token')
+ if (args.length === 0) return withCb(list([]), cb)
+ switch (args[0]) {
+ case 'list':
+ case 'ls':
+ withCb(list(), cb)
+ break
+ case 'delete':
+ case 'rel':
+ case 'remove':
+ case 'rm':
+ withCb(rm(args.slice(1)), cb)
+ break
+ case 'create':
+ withCb(create(args.slice(1)), cb)
+ break
+ default:
+ cb(new Error('Unknown profile command: ' + args[0]))
+ }
+}
+
+function generateTokenIds (tokens, minLength) {
+ const byId = {}
+ tokens.forEach((token) => {
+ token.id = token.key
+ for (let ii = minLength; ii < token.key.length; ++ii) {
+ if (!tokens.some((ot) => ot !== token && ot.key.slice(0, ii) === token.key.slice(0, ii))) {
+ token.id = token.key.slice(0, ii)
+ break
+ }
+ }
+ byId[token.id] = token
+ })
+ return byId
+}
+
+function config () {
+ const conf = {
+ json: npm.config.get('json'),
+ parseable: npm.config.get('parseable'),
+ registry: npm.config.get('registry'),
+ otp: npm.config.get('otp')
+ }
+ conf.auth = npm.config.getCredentialsByURI(conf.registry)
+ if (conf.otp) conf.auth.otp = conf.otp
+ return conf
+}
+
+function list (args) {
+ const conf = config()
+ log.info('token', 'getting list')
+ return pulseTillDone.withPromise(profile.listTokens(conf)).then((tokens) => {
+ if (conf.json) {
+ output(JSON.stringify(tokens, null, 2))
+ return
+ } else if (conf.parseable) {
+ output(['key', 'token', 'created', 'readonly', 'CIDR whitelist'].join('\t'))
+ tokens.forEach((token) => {
+ output([
+ token.key,
+ token.token,
+ token.created,
+ token.readonly ? 'true' : 'false',
+ token.cidr_whitelist ? token.cidr_whitelist.join(',') : ''
+ ].join('\t'))
+ })
+ return
+ }
+ generateTokenIds(tokens, 6)
+ const idWidth = tokens.reduce((acc, token) => Math.max(acc, token.id.length), 0)
+ const table = new Table({
+ head: ['id', 'token', 'created', 'readonly', 'CIDR whitelist'],
+ colWidths: [Math.max(idWidth, 2) + 2, 9, 12, 10]
+ })
+ tokens.forEach((token) => {
+ table.push([
+ token.id,
+ token.token + '…',
+ String(token.created).slice(0, 10),
+ token.readonly ? 'yes' : 'no',
+ token.cidr_whitelist ? token.cidr_whitelist.join(', ') : ''
+ ])
+ })
+ output(table.toString())
+ })
+}
+
+function rm (args) {
+ if (args.length === 0) {
+ throw new Error('npm token delete <tokenKey>')
+ }
+ const conf = config()
+ const toRemove = []
+ const progress = log.newItem('removing tokens', toRemove.length)
+ progress.info('token', 'getting existing list')
+ return pulseTillDone.withPromise(profile.listTokens(conf).then((tokens) => {
+ args.forEach((id) => {
+ const matches = tokens.filter((token) => token.key.indexOf(id) === 0)
+ if (matches.length === 1) {
+ toRemove.push(matches[0].key)
+ } else if (matches.length > 1) {
+ throw new Error(`Token ID "${id}" was ambiguous, a new token may have been created since you last ran \`npm-profile token list\`.`)
+ } else {
+ const tokenMatches = tokens.filter((token) => id.indexOf(token.token) === 0)
+ if (tokenMatches === 0) {
+ throw new Error(`Unknown token id or value "${id}".`)
+ }
+ toRemove.push(id)
+ }
+ })
+ return Bluebird.map(toRemove, (key) => {
+ progress.info('token', 'removing', key)
+ profile.removeToken(key, conf).then(() => profile.completeWork(1))
+ })
+ })).then(() => {
+ if (conf.json) {
+ output(JSON.stringify(toRemove))
+ } else if (conf.parseable) {
+ output(toRemove.join('\t'))
+ } else {
+ output('Removed ' + toRemove.length + ' token' + (toRemove.length !== 1 ? 's' : ''))
+ }
+ })
+}
+
+function create (args) {
+ const conf = config()
+ const cidr = npm.config.get('cidr')
+ const readonly = npm.config.get('read-only')
+
+ const validCIDR = validateCIDRList(cidr)
+ return readUserInfo.password().then((password) => {
+ log.info('token', 'creating')
+ return profile.createToken(password, readonly, validCIDR, conf).catch((ex) => {
+ if (ex.code !== 'EOTP') throw ex
+ log.info('token', 'failed because it requires OTP')
+ return readUserInfo.otp('Authenticator provided OTP:').then((otp) => {
+ conf.auth.otp = otp
+ log.info('token', 'creating with OTP')
+ return pulseTillDone.withPromise(profile.createToken(password, readonly, validCIDR, conf))
+ })
+ })
+ }).then((result) => {
+ delete result.key
+ delete result.updated
+ if (conf.json) {
+ output(JSON.stringify(result))
+ } else if (conf.parseable) {
+ Object.keys(result).forEach((k) => output(k + '\t' + result[k]))
+ } else {
+ const table = new Table()
+ Object.keys(result).forEach((k) => table.push({[ansistyles.bright(k)]: String(result[k])}))
+ output(table.toString())
+ }
+ })
+}
+
+function validateCIDR (cidr) {
+ if (isCidrV6(cidr)) {
+ throw new Error('CIDR whitelist can only contain IPv4 addresses, ' + cidr + ' is IPv6')
+ }
+ if (!isCidrV4(cidr)) {
+ throw new Error('CIDR whitelist contains invalid CIDR entry: ' + cidr)
+ }
+}
+
+function validateCIDRList (cidrs) {
+ const list = Array.isArray(cidrs) ? cidrs : cidrs ? cidrs.split(/,\s*/) : []
+ list.forEach(validateCIDR)
+ return list
+}
diff --git a/lib/utils/pulse-till-done.js b/lib/utils/pulse-till-done.js
index 266924130..b292c2fa5 100644
--- a/lib/utils/pulse-till-done.js
+++ b/lib/utils/pulse-till-done.js
@@ -1,22 +1,38 @@
'use strict'
-var validate = require('aproba')
-var log = require('npmlog')
+const validate = require('aproba')
+const log = require('npmlog')
+const Bluebird = require('bluebird')
-var pulsers = 0
-var pulse
+let pulsers = 0
+let pulse
+
+function pulseStart (prefix) {
+ if (++pulsers > 1) return
+ pulse = setInterval(function () {
+ log.gauge.pulse(prefix)
+ }, 150)
+}
+function pulseStop () {
+ if (--pulsers > 0) return
+ clearInterval(pulse)
+}
module.exports = function (prefix, cb) {
validate('SF', [prefix, cb])
if (!prefix) prefix = 'network'
- if (!pulsers++) {
- pulse = setInterval(function () {
- log.gauge.pulse(prefix)
- }, 250)
- }
+ pulseStart(prefix)
return function () {
- if (!--pulsers) {
- clearInterval(pulse)
- }
+ pulseStop()
cb.apply(null, arguments)
}
}
+module.exports.withPromise = pulseWhile
+
+function pulseWhile (prefix, promise) {
+ if (!promise) {
+ promise = prefix
+ prefix = ''
+ }
+ pulseStart(prefix)
+ return Bluebird.resolve(promise).finally(() => pulseStop())
+}
diff --git a/lib/utils/read-user-info.js b/lib/utils/read-user-info.js
new file mode 100644
index 000000000..359432cf7
--- /dev/null
+++ b/lib/utils/read-user-info.js
@@ -0,0 +1,66 @@
+'use strict'
+const Bluebird = require('bluebird')
+const readAsync = Bluebird.promisify(require('read'))
+const userValidate = require('npm-user-validate')
+const log = require('npmlog')
+
+exports.otp = readOTP
+exports.password = readPassword
+exports.username = readUsername
+exports.email = readEmail
+
+function read (opts) {
+ return Bluebird.try(() => {
+ log.clearProgress()
+ return readAsync(opts)
+ }).finally(() => {
+ log.showProgress()
+ })
+}
+
+function readOTP (msg, otp, isRetry) {
+ if (!msg) msg = 'Enter OTP: '
+ if (isRetry && otp && /^[\d ]+$|^[A-Fa-f0-9]{64,64}$/.test(otp)) return otp.replace(/\s+/g, '')
+
+ return read({prompt: msg, default: otp || ''})
+ .then((otp) => readOTP(msg, otp, true))
+}
+
+function readPassword (msg, password, isRetry) {
+ if (!msg) msg = 'npm password: '
+ if (isRetry && password) return password
+
+ return read({prompt: msg, silent: true, default: password || ''})
+ .then((password) => readPassword(msg, password, true))
+}
+
+function readUsername (msg, username, opts, isRetry) {
+ if (!msg) msg = 'npm username: '
+ if (isRetry && username) {
+ const error = userValidate.username(username)
+ if (error) {
+ opts.log && opts.log.warn(error.message)
+ } else {
+ return Promise.resolve(username.trim())
+ }
+ }
+
+ return read({prompt: msg, default: username || ''})
+ .then((username) => readUsername(msg, username, opts, true))
+}
+
+function readEmail (msg, email, opts, isRetry) {
+ if (!msg) msg = 'email (this IS public): '
+ if (isRetry && email) {
+ const error = userValidate.email(email)
+ if (error) {
+ opts.log && opts.log.warn(error.message)
+ } else {
+ return email.trim()
+ }
+ }
+
+ return read({prompt: msg, default: email || ''})
+ .then((username) => readEmail(msg, username, opts, true))
+}
+