• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const profile = require('libnpm/profile')
4const npm = require('./npm.js')
5const figgyPudding = require('figgy-pudding')
6const npmConfig = require('./config/figgy-config.js')
7const output = require('./utils/output.js')
8const otplease = require('./utils/otplease.js')
9const Table = require('cli-table3')
10const Bluebird = require('bluebird')
11const isCidrV4 = require('is-cidr').v4
12const isCidrV6 = require('is-cidr').v6
13const readUserInfo = require('./utils/read-user-info.js')
14const ansistyles = require('ansistyles')
15const log = require('npmlog')
16const pulseTillDone = require('./utils/pulse-till-done.js')
17
18module.exports = token
19
20token._validateCIDRList = validateCIDRList
21
22token.usage =
23  'npm token list\n' +
24  'npm token revoke <tokenKey>\n' +
25  'npm token create [--read-only] [--cidr=list]\n'
26
27token.subcommands = ['list', 'revoke', 'create']
28
29token.completion = function (opts, cb) {
30  var argv = opts.conf.argv.remain
31
32  switch (argv[2]) {
33    case 'list':
34    case 'revoke':
35    case 'create':
36      return cb(null, [])
37    default:
38      return cb(new Error(argv[2] + ' not recognized'))
39  }
40}
41
42function withCb (prom, cb) {
43  prom.then((value) => cb(null, value), cb)
44}
45
46function token (args, cb) {
47  log.gauge.show('token')
48  if (args.length === 0) return withCb(list([]), cb)
49  switch (args[0]) {
50    case 'list':
51    case 'ls':
52      withCb(list(), cb)
53      break
54    case 'delete':
55    case 'revoke':
56    case 'remove':
57    case 'rm':
58      withCb(rm(args.slice(1)), cb)
59      break
60    case 'create':
61      withCb(create(args.slice(1)), cb)
62      break
63    default:
64      cb(new Error('Unknown profile command: ' + args[0]))
65  }
66}
67
68function generateTokenIds (tokens, minLength) {
69  const byId = {}
70  tokens.forEach((token) => {
71    token.id = token.key
72    for (let ii = minLength; ii < token.key.length; ++ii) {
73      if (!tokens.some((ot) => ot !== token && ot.key.slice(0, ii) === token.key.slice(0, ii))) {
74        token.id = token.key.slice(0, ii)
75        break
76      }
77    }
78    byId[token.id] = token
79  })
80  return byId
81}
82
83const TokenConfig = figgyPudding({
84  auth: {},
85  registry: {},
86  otp: {},
87  cidr: {},
88  'read-only': {},
89  json: {},
90  parseable: {}
91})
92
93function config () {
94  let conf = TokenConfig(npmConfig())
95  const creds = npm.config.getCredentialsByURI(conf.registry)
96  if (creds.token) {
97    conf = conf.concat({
98      auth: { token: creds.token }
99    })
100  } else if (creds.username) {
101    conf = conf.concat({
102      auth: {
103        basic: {
104          username: creds.username,
105          password: creds.password
106        }
107      }
108    })
109  } else if (creds.auth) {
110    const auth = Buffer.from(creds.auth, 'base64').toString().split(':', 2)
111    conf = conf.concat({
112      auth: {
113        basic: {
114          username: auth[0],
115          password: auth[1]
116        }
117      }
118    })
119  } else {
120    conf = conf.concat({ auth: {} })
121  }
122  if (conf.otp) conf.auth.otp = conf.otp
123  return conf
124}
125
126function list (args) {
127  const conf = config()
128  log.info('token', 'getting list')
129  return pulseTillDone.withPromise(profile.listTokens(conf)).then((tokens) => {
130    if (conf.json) {
131      output(JSON.stringify(tokens, null, 2))
132      return
133    } else if (conf.parseable) {
134      output(['key', 'token', 'created', 'readonly', 'CIDR whitelist'].join('\t'))
135      tokens.forEach((token) => {
136        output([
137          token.key,
138          token.token,
139          token.created,
140          token.readonly ? 'true' : 'false',
141          token.cidr_whitelist ? token.cidr_whitelist.join(',') : ''
142        ].join('\t'))
143      })
144      return
145    }
146    generateTokenIds(tokens, 6)
147    const idWidth = tokens.reduce((acc, token) => Math.max(acc, token.id.length), 0)
148    const table = new Table({
149      head: ['id', 'token', 'created', 'readonly', 'CIDR whitelist'],
150      colWidths: [Math.max(idWidth, 2) + 2, 9, 12, 10]
151    })
152    tokens.forEach((token) => {
153      table.push([
154        token.id,
155        token.token + '…',
156        String(token.created).slice(0, 10),
157        token.readonly ? 'yes' : 'no',
158        token.cidr_whitelist ? token.cidr_whitelist.join(', ') : ''
159      ])
160    })
161    output(table.toString())
162  })
163}
164
165function rm (args) {
166  if (args.length === 0) {
167    throw new Error('npm token revoke <tokenKey>')
168  }
169  const conf = config()
170  const toRemove = []
171  const progress = log.newItem('removing tokens', toRemove.length)
172  progress.info('token', 'getting existing list')
173  return pulseTillDone.withPromise(profile.listTokens(conf).then((tokens) => {
174    args.forEach((id) => {
175      const matches = tokens.filter((token) => token.key.indexOf(id) === 0)
176      if (matches.length === 1) {
177        toRemove.push(matches[0].key)
178      } else if (matches.length > 1) {
179        throw new Error(`Token ID "${id}" was ambiguous, a new token may have been created since you last ran \`npm-profile token list\`.`)
180      } else {
181        const tokenMatches = tokens.filter((token) => id.indexOf(token.token) === 0)
182        if (tokenMatches === 0) {
183          throw new Error(`Unknown token id or value "${id}".`)
184        }
185        toRemove.push(id)
186      }
187    })
188    return Bluebird.map(toRemove, (key) => {
189      return otplease(conf, conf => {
190        return profile.removeToken(key, conf)
191      })
192    })
193  })).then(() => {
194    if (conf.json) {
195      output(JSON.stringify(toRemove))
196    } else if (conf.parseable) {
197      output(toRemove.join('\t'))
198    } else {
199      output('Removed ' + toRemove.length + ' token' + (toRemove.length !== 1 ? 's' : ''))
200    }
201  })
202}
203
204function create (args) {
205  const conf = config()
206  const cidr = conf.cidr
207  const readonly = conf['read-only']
208
209  const validCIDR = validateCIDRList(cidr)
210  return readUserInfo.password().then((password) => {
211    log.info('token', 'creating')
212    return pulseTillDone.withPromise(otplease(conf, conf => {
213      return profile.createToken(password, readonly, validCIDR, conf)
214    }))
215  }).then((result) => {
216    delete result.key
217    delete result.updated
218    if (conf.json) {
219      output(JSON.stringify(result))
220    } else if (conf.parseable) {
221      Object.keys(result).forEach((k) => output(k + '\t' + result[k]))
222    } else {
223      const table = new Table()
224      Object.keys(result).forEach((k) => table.push({[ansistyles.bright(k)]: String(result[k])}))
225      output(table.toString())
226    }
227  })
228}
229
230function validateCIDR (cidr) {
231  if (isCidrV6(cidr)) {
232    throw new Error('CIDR whitelist can only contain IPv4 addresses, ' + cidr + ' is IPv6')
233  }
234  if (!isCidrV4(cidr)) {
235    throw new Error('CIDR whitelist contains invalid CIDR entry: ' + cidr)
236  }
237}
238
239function validateCIDRList (cidrs) {
240  const maybeList = cidrs ? (Array.isArray(cidrs) ? cidrs : [cidrs]) : []
241  const list = maybeList.length === 1 ? maybeList[0].split(/,\s*/) : maybeList
242  list.forEach(validateCIDR)
243  return list
244}
245