Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
profile.js367 linesDownload Raw Back to commands
1const { inspect } = require('node:util')
2const { URL } = require('node:url')
3const { log, output } = require('proc-log')
4const { get, set, createToken } = require('npm-profile')
5const qrcodeTerminal = require('qrcode-terminal')
6const { otplease } = require('../utils/auth.js')
7const readUserInfo = require('../utils/read-user-info.js')
8const BaseCommand = require('../base-cmd.js')
9
10const qrcode = url =>
11  new Promise((resolve) => qrcodeTerminal.generate(url, resolve))
12
13const knownProfileKeys = [
14  'name',
15  'email',
16  'two-factor auth',
17  'fullname',
18  'homepage',
19  'freenode',
20  'twitter',
21  'github',
22  'created',
23  'updated',
24]
25
26const writableProfileKeys = [
27  'email',
28  'password',
29  'fullname',
30  'homepage',
31  'freenode',
32  'twitter',
33  'github',
34]
35
36class Profile extends BaseCommand {
37  static description = 'Change settings on your registry profile'
38  static name = 'profile'
39  static usage = [
40    'enable-2fa [auth-only|auth-and-writes]',
41    'disable-2fa',
42    'get [<key>]',
43    'set <key> <value>',
44  ]
45
46  static params = [
47    'registry',
48    'json',
49    'parseable',
50    'otp',
51  ]
52
53  static async completion (opts) {
54    var argv = opts.conf.argv.remain
55
56    if (!argv[2]) {
57      return ['enable-2fa', 'disable-2fa', 'get', 'set']
58    }
59
60    switch (argv[2]) {
61      case 'enable-2fa':
62      case 'enable-tfa':
63        return ['auth-and-writes', 'auth-only']
64
65      case 'disable-2fa':
66      case 'disable-tfa':
67      case 'get':
68      case 'set':
69        return []
70      default:
71        throw new Error(argv[2] + ' not recognized')
72    }
73  }
74
75  async exec (args) {
76    if (args.length === 0) {
77      throw this.usageError()
78    }
79
80    const [subcmd, ...opts] = args
81
82    switch (subcmd) {
83      case 'enable-2fa':
84      case 'enable-tfa':
85      case 'enable2fa':
86      case 'enabletfa':
87        return this.enable2fa(opts)
88      case 'disable-2fa':
89      case 'disable-tfa':
90      case 'disable2fa':
91      case 'disabletfa':
92        return this.disable2fa()
93      case 'get':
94        return this.get(opts)
95      case 'set':
96        return this.set(opts)
97      default:
98        throw new Error('Unknown profile command: ' + subcmd)
99    }
100  }
101
102  async get (args) {
103    const tfa = 'two-factor auth'
104    const info = await get({ ...this.npm.flatOptions })
105
106    if (!info.cidr_whitelist) {
107      delete info.cidr_whitelist
108    }
109
110    if (this.npm.config.get('json')) {
111      output.buffer(info)
112      return
113    }
114
115    // clean up and format key/values for output
116    const cleaned = {}
117    for (const key of knownProfileKeys) {
118      cleaned[key] = info[key] || ''
119    }
120
121    const unknownProfileKeys = Object.keys(info).filter((k) => !(k in cleaned))
122    for (const key of unknownProfileKeys) {
123      cleaned[key] = info[key] || ''
124    }
125
126    delete cleaned.tfa
127    delete cleaned.email_verified
128    cleaned.email += info.email_verified ? ' (verified)' : '(unverified)'
129
130    if (info.tfa && !info.tfa.pending) {
131      cleaned[tfa] = info.tfa.mode
132    } else {
133      cleaned[tfa] = 'disabled'
134    }
135
136    if (args.length) {
137      const values = args // comma or space separated
138        .join(',')
139        .split(/,/)
140        .filter((arg) => arg.trim() !== '')
141        .map((arg) => cleaned[arg])
142        .join('\t')
143      output.standard(values)
144    } else {
145      if (this.npm.config.get('parseable')) {
146        for (const key of Object.keys(info)) {
147          if (key === 'tfa') {
148            output.standard(`${key}\t${cleaned[tfa]}`)
149          } else {
150            output.standard(`${key}\t${info[key]}`)
151          }
152        }
153      } else {
154        for (const [key, value] of Object.entries(cleaned)) {
155          output.standard(`${key}: ${value}`)
156        }
157      }
158    }
159  }
160
161  async set (args) {
162    const conf = { ...this.npm.flatOptions }
163    const prop = (args[0] || '').toLowerCase().trim()
164
165    let value = args.length > 1 ? args.slice(1).join(' ') : null
166
167    const readPasswords = async () => {
168      const newpassword = await readUserInfo.password('New password: ')
169      const confirmedpassword = await readUserInfo.password('       Again:     ')
170
171      if (newpassword !== confirmedpassword) {
172        log.warn('profile', 'Passwords do not match, please try again.')
173        return readPasswords()
174      }
175
176      return newpassword
177    }
178
179    if (prop !== 'password' && value === null) {
180      throw new Error('npm profile set <prop> <value>')
181    }
182
183    if (prop === 'password' && value !== null) {
184      throw new Error(
185        'npm profile set password\n' +
186        'Do not include your current or new passwords on the command line.')
187    }
188
189    if (writableProfileKeys.indexOf(prop) === -1) {
190      throw new Error(`"${prop}" is not a property we can set. ` +
191        `Valid properties are: ` + writableProfileKeys.join(', '))
192    }
193
194    if (prop === 'password') {
195      const current = await readUserInfo.password('Current password: ')
196      const newpassword = await readPasswords()
197
198      value = { old: current, new: newpassword }
199    }
200
201    // FIXME: Work around to not clear everything other than what we're setting
202    const user = await get(conf)
203    const newUser = {}
204
205    for (const key of writableProfileKeys) {
206      newUser[key] = user[key]
207    }
208
209    newUser[prop] = value
210
211    const result = await otplease(this.npm, conf, c => set(newUser, c))
212
213    if (this.npm.config.get('json')) {
214      output.buffer({ [prop]: result[prop] })
215    } else if (this.npm.config.get('parseable')) {
216      output.standard(prop + '\t' + result[prop])
217    } else if (result[prop] != null) {
218      output.standard('Set', prop, 'to', result[prop])
219    } else {
220      output.standard('Set', prop)
221    }
222  }
223
224  async enable2fa (args) {
225    const conf = { ...this.npm.flatOptions }
226
227    if (args.length > 1) {
228      throw new Error('npm profile enable-2fa [auth-and-writes|auth-only]')
229    }
230
231    const mode = args[0] || 'auth-and-writes'
232    if (mode !== 'auth-only' && mode !== 'auth-and-writes') {
233      throw new Error(
234        `Invalid two-factor authentication mode "${mode}".\n` +
235        'Valid modes are:\n' +
236        '  auth-only - Require two-factor authentication only when logging in\n' +
237        '  auth-and-writes - Require two-factor authentication when logging in ' +
238        'AND when publishing'
239      )
240    }
241
242    if (this.npm.config.get('json') || this.npm.config.get('parseable')) {
243      throw new Error(
244        'Enabling two-factor authentication is an interactive operation and ' +
245        (this.npm.config.get('json') ? 'JSON' : 'parseable') + ' output mode is not available'
246      )
247    }
248
249    const userInfo = await get(conf)
250
251    if (!userInfo?.tfa?.pending && userInfo?.tfa?.mode === mode) {
252      output.standard('Two factor authentication is already enabled and set to ' + mode)
253      return
254    }
255
256    const info = {
257      tfa: {
258        mode,
259      },
260    }
261
262    // if they're using legacy auth currently then we have to update them to a bearer token before continuing.
263    const creds = this.npm.config.getCredentialsByURI(this.npm.config.get('registry'))
264    const auth = {}
265
266    if (creds.token) {
267      auth.token = creds.token
268    } else if (creds.username) {
269      auth.basic = { username: creds.username, password: creds.password }
270    } else if (creds.auth) {
271      const basic = Buffer.from(creds.auth, 'base64').toString().split(':', 2)
272      auth.basic = { username: basic[0], password: basic[1] }
273    }
274
275    if (!auth.basic && !auth.token) {
276      throw new Error(
277        'You need to be logged in to registry ' +
278        `${this.npm.config.get('registry')} in order to enable 2fa`
279      )
280    }
281
282    if (auth.basic) {
283      log.info('profile', 'Updating authentication to bearer token')
284      const result = await createToken(
285        auth.basic.password, false, [], { ...this.npm.flatOptions }
286      )
287
288      if (!result.token) {
289        throw new Error(`Your registry ${this.npm.config.get('registry')} does not seem to support bearer tokens. Bearer tokens are required for two-factor authentication.`)
290      }
291
292      this.npm.config.setCredentialsByURI(
293        this.npm.config.get('registry'),
294        { token: result.token }
295      )
296      await this.npm.config.save('user')
297    }
298
299    log.notice('profile', 'Enabling two factor authentication for ' + mode)
300    const password = await readUserInfo.password()
301    info.tfa.password = password
302
303    if (userInfo && userInfo.tfa && userInfo.tfa.pending) {
304      log.info('profile', 'Resetting two-factor authentication')
305      await set({ tfa: { password, mode: 'disable' } }, conf)
306    }
307
308    log.info('profile', 'Setting two-factor authentication to ' + mode)
309    const challenge = await otplease(this.npm, conf, o => set(info, o))
310
311    if (challenge.tfa && challenge.tfa.mode) {
312      output.standard('Two factor authentication mode changed to: ' + mode)
313      return
314    }
315
316    const badResponse = typeof challenge.tfa !== 'string'
317      || !/^otpauth:[/][/]/.test(challenge.tfa)
318    if (badResponse) {
319      throw new Error(`Unknown error enabling two-factor authentication. Expected otpauth URL, got: ${inspect(challenge.tfa)}`)
320    }
321
322    const otpauth = new URL(challenge.tfa)
323    const secret = otpauth.searchParams.get('secret')
324    const code = await qrcode(challenge.tfa)
325
326    output.standard('Scan into your authenticator app:\n' + code + '\n Or enter code:', secret)
327
328    const interactiveOTP = await readUserInfo.otp('And an OTP code from your authenticator: ')
329
330    log.info('profile', 'Finalizing two-factor authentication')
331
332    const result = await set({ tfa: [interactiveOTP] }, conf)
333
334    output.standard('2FA successfully enabled. Below are your recovery codes, please print these out.')
335    output.standard('You will need these to recover access to your account if you lose your authentication device.')
336
337    for (const tfaCode of result.tfa) {
338      output.standard('\t' + tfaCode)
339    }
340  }
341
342  async disable2fa () {
343    const opts = { ...this.npm.flatOptions }
344    const info = await get(opts)
345
346    if (!info.tfa || info.tfa.pending) {
347      output.standard('Two factor authentication not enabled.')
348      return
349    }
350
351    const password = await readUserInfo.password()
352
353    log.info('profile', 'disabling tfa')
354    await otplease(this.npm, opts, o => set({ tfa: { password: password, mode: 'disable' } }, o))
355
356    if (this.npm.config.get('json')) {
357      output.buffer({ tfa: false })
358    } else if (this.npm.config.get('parseable')) {
359      output.standard('tfa\tfalse')
360    } else {
361      output.standard('Two factor authentication disabled.')
362    }
363  }
364}
365
366module.exports = Profile
367 
codekingpro/portable-devtools · Team Ai